Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add UDF to study and endpoint forms #906

Merged
merged 50 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
573d242
render udf form inside study form
munnsmunns Sep 27, 2023
251ca10
Add model for saved data from UDF
munnsmunns Sep 27, 2023
5d415a3
remove duplicate widget
munnsmunns Sep 27, 2023
6bb8aa9
add todo for form tag issue
munnsmunns Sep 27, 2023
8b2e557
render saved UDF data on study detail page
munnsmunns Sep 29, 2023
9b29d5d
add mixin for adding UDF to detail pages
munnsmunns Sep 29, 2023
d1aa36d
lint
munnsmunns Sep 29, 2023
a13297b
backport changes for demo
munnsmunns Oct 27, 2023
2ba08cc
reduce queries in forms w/ UDF
munnsmunns Oct 27, 2023
8df57db
changes from review
munnsmunns Oct 27, 2023
4fc4bfe
remove generic relation
munnsmunns Oct 27, 2023
7c56a66
remove import
munnsmunns Oct 27, 2023
566e3dd
Merge branch 'main' of https://github.com/shapiromatron/hawc into udf…
munnsmunns Oct 27, 2023
c74a8aa
fix test now that rendering works properly
munnsmunns Oct 27, 2023
03acb88
fix udf detail pages
munnsmunns Oct 27, 2023
5954639
move fixture modelbinding to different assessment
munnsmunns Oct 27, 2023
8cb1286
fix tests
munnsmunns Oct 31, 2023
65af322
Merge branch 'main' into udf-render-forms
munnsmunns Oct 31, 2023
e811501
add dynamicformlisteners to necessary pages
munnsmunns Nov 1, 2023
54cdb16
Merge branch 'main' into udf-render-forms
caseyhans Jan 3, 2024
4b8a0cd
create UDFCache class
munnsmunns Jan 8, 2024
ef2de3c
add cache to endpoint form
munnsmunns Jan 8, 2024
b546ddf
set udf contents when form is saved
munnsmunns Jan 8, 2024
d4229cd
integrate cache into detail pages
munnsmunns Jan 8, 2024
e966000
delete cache for modelbinding post save
munnsmunns Jan 8, 2024
3c9b5f1
add caching to study form
munnsmunns Jan 8, 2024
26516d8
clean up cache class
munnsmunns Jan 8, 2024
d2240d5
fix content cache to work on detail pages
munnsmunns Jan 8, 2024
4b5cd46
fix attribute error and add type hints
munnsmunns Jan 8, 2024
59c685d
Merge branch 'main' into udf-render-forms
munnsmunns Jan 8, 2024
14ffe25
Merge branch 'main' into udf-render-forms
caseyhans Jan 11, 2024
80390a7
Merge branch 'main' into udf-render-forms
caseyhans Jan 12, 2024
9ca5b5b
Merge branch 'main' into udf-render-forms
caseyhans Jan 16, 2024
bd3be8b
Merge branch 'main' into udf-render-forms
caseyhans Jan 23, 2024
ba908bd
Merge branch 'main' into udf-render-forms
caseyhans Feb 9, 2024
3edb16e
format templates
caseyhans Feb 9, 2024
ea14922
fix curlies, change field name
caseyhans Feb 14, 2024
707db34
Merge branch 'main' into udf-render-forms
shapiromatron Mar 11, 2024
c242a7d
reformat test html
shapiromatron Mar 12, 2024
9a2f730
refactor common code into a form mixin
shapiromatron Mar 12, 2024
e445f72
load JS with DynamicForm or DynamicFormWidget
shapiromatron Mar 12, 2024
5534d61
add db constraint
shapiromatron Mar 12, 2024
b667ecb
rename UDF table fragment
shapiromatron Mar 12, 2024
2fe1f62
fix table template layout
shapiromatron Mar 12, 2024
6cc928d
fix unique-together constraint
shapiromatron Mar 12, 2024
780ef15
fix signals
shapiromatron Mar 12, 2024
518b4f5
bonus - improve type annotations for cacheable
shapiromatron Mar 12, 2024
1f36963
refactor cache
shapiromatron Mar 12, 2024
0a85e9d
refactor content list
shapiromatron Mar 12, 2024
10cb863
add tests
shapiromatron Mar 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions hawc/apps/assessment/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist
from django.core.validators import MinValueValidator
from django.db import models
from django.http import HttpRequest
Expand Down Expand Up @@ -314,6 +315,19 @@ def get_assessment_logs_url(self):
def get_udf_list_url(self):
return reverse("udf:binding-list", args=(self.id,))

def get_model_udf(self, model: type[models.Model] | models.Model, *args, **kwargs):
"""Get the form instance from this assessment's UDF for the given model class/instance.

Args:
model: a model class or an instance of a model that has a UDF bound to it in this
assessment.
"""
content_type = ContentType.objects.get_for_model(model)
try:
return self.udf_bindings.get(content_type=content_type).form_instance(*args, **kwargs)
except ObjectDoesNotExist:
return None

def get_clear_cache_url(self):
return reverse("assessment:clear_cache", args=(self.id,))

Expand Down
32 changes: 0 additions & 32 deletions hawc/apps/common/dynamic_forms/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,35 +90,3 @@ def auto_wrap_fields(self):

self[:].wrap_together(cfl.Row)
self.add_field_wraps()


class DynamicFormWidget(forms.Widget):
"""Widget to display dynamic form inline."""

template_name = "common/widgets/dynamic_form.html"

def __init__(self, prefix, form_class, form_kwargs=None, *args, **kwargs):
"""Create dynamic form widget."""
super().__init__(*args, **kwargs)
self.prefix = prefix
self.form_class = form_class
if form_kwargs is None:
form_kwargs = {}
self.form_kwargs = {"prefix": prefix, **form_kwargs}

def add_prefix(self, field_name):
"""Add prefix in the same way Django forms add prefixes."""
return f"{self.prefix}-{field_name}"

def format_value(self, value):
"""Value used in rendering."""
value = json.loads(value)
if value:
value = {self.add_prefix(k): v for k, v in value.items()}
return self.form_class(data=value, **self.form_kwargs)

def value_from_datadict(self, data, files, name):
"""Parse value from POST request."""
form = self.form_class(data=data, **self.form_kwargs)
form.full_clean()
return form.cleaned_data
5 changes: 5 additions & 0 deletions hawc/apps/study/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,16 @@ def __init__(self, *args, **kwargs):
self.instance.assessment = parent
elif type(parent) is Reference:
self.instance.reference_ptr = parent
assessment = self.instance.get_assessment()
udf = assessment.get_model_udf(self.Meta.model, label="User defined fields")
if udf:
self.fields["udf"] = udf
if self.instance:
self.fields["internal_communications"].initial = self.instance.get_communications()

self.helper = self.setHelper()

# TODO: For some reason, form actions div is being 'pushed' outside the form tag
def setHelper(self, inputs: dict | None = None):
if inputs is None:
inputs = {}
Expand Down
4 changes: 4 additions & 0 deletions hawc/apps/study/templates/study/study_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ <h3>{{assessment.get_rob_name_display}}</h3>
{% include "eco/fragments/_design_list.html" with object_list=object.eco_designs.all %}
{% endif %}

{% if udf_content %}
munnsmunns marked this conversation as resolved.
Show resolved Hide resolved
{% include "udf/fragments/_udf_content.html" with object=udf_content %}
{% endif %}

{% endif %}

{% endblock %}
Expand Down
9 changes: 8 additions & 1 deletion hawc/apps/study/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.apps import apps
from django.core.exceptions import PermissionDenied
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied, ObjectDoesNotExist
from django.db import transaction
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect
Expand Down Expand Up @@ -128,6 +129,12 @@ def get_context_data(self, **kwargs):
"attachments": self.object.get_attachments_dict() if attachments_viewable else None,
}
context["internal_communications"] = self.object.get_communications()
content_type = ContentType.objects.get_for_model(self.model)
try:
udf_binding = self.assessment.udf_bindings.get(content_type=content_type)
context["udf_content"] = udf_binding.saved_contents.get(object_id=self.object.pk)
except ObjectDoesNotExist:
context["udf_content"] = None
return context


Expand Down
6 changes: 6 additions & 0 deletions hawc/apps/udf/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ class TagBindingInline(admin.TabularInline):
extra = 0


class ModelUDFContentInline(admin.TabularInline):
model = models.ModelUDFContent
extra = 0


admin.site.register(models.UserDefinedForm)
admin.site.register(models.ModelBinding)
admin.site.register(models.TagBinding)
admin.site.register(models.ModelUDFContent)
43 changes: 43 additions & 0 deletions hawc/apps/udf/migrations/0003_modeludfcontent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Generated by Django 4.2.4 on 2023-09-27 20:58

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("udf", "0002_tagbinding_modelbinding"),
]

operations = [
migrations.CreateModel(
name="ModelUDFContent",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("object_id", models.PositiveIntegerField(null=True)),
("content", models.JSONField(blank=True, default=dict)),
("created", models.DateTimeField(auto_now_add=True)),
("last_updated", models.DateTimeField(auto_now=True)),
(
"content_type",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="contenttypes.contenttype"
),
),
(
"model_binding",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="saved_contents",
to="udf.modelbinding",
),
),
],
),
]
63 changes: 58 additions & 5 deletions hawc/apps/udf/models.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import reversion
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.forms import Form
from django.forms import JSONField
from django.urls import reverse

from ..assessment.models import Assessment
from ..common import dynamic_forms
from ..common.forms import DynamicFormField
from ..lit.models import ReferenceFilterTag


Expand All @@ -33,6 +35,9 @@ class Meta:
unique_together = (("creator", "name"),)
ordering = ("-last_updated",)

def __str__(self):
return f"{self.name}"

def get_absolute_url(self):
return reverse("udf:udf_detail", args=(self.pk,))

Expand All @@ -58,8 +63,15 @@ class Meta:
indexes = (models.Index(fields=["assessment", "content_type"]),)
unique_together = (("assessment", "content_type"),)

def form_instance(self) -> Form:
return dynamic_forms.Schema.parse_obj(self.form.schema).to_form()
def __str__(self):
return f"{self.assessment}/{self.content_type.model} form"

def form_instance(self, *args, **kwargs) -> JSONField | DynamicFormField:
prefix = kwargs.pop("prefix", "udf")
form_kwargs = kwargs.pop("form_kwargs", None)
return dynamic_forms.Schema.parse_obj(self.form.schema).to_form_field(
prefix, form_kwargs, *args, **kwargs
)

def get_assessment(self):
return self.assessment
Expand All @@ -86,8 +98,12 @@ class Meta:
indexes = (models.Index(fields=["assessment", "tag"]),)
unique_together = (("assessment", "tag"),)

def form_instance(self) -> Form:
return dynamic_forms.Schema.parse_obj(self.form.schema).to_form()
def form_instance(
self, prefix="", form_kwargs=None, *args, **kwargs
) -> JSONField | DynamicFormField:
return dynamic_forms.Schema.parse_obj(self.form.schema).to_form_field(
prefix, form_kwargs, *args, **kwargs
)

def get_assessment(self):
return self.assessment
Expand All @@ -96,6 +112,43 @@ def get_absolute_url(self):
return reverse("udf:tag_detail", args=(self.id,))


class ModelUDFContent(models.Model):
model_binding = models.ForeignKey(
ModelBinding, on_delete=models.CASCADE, related_name="saved_contents"
)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField(null=True)
content_object = GenericForeignKey(
"content_type",
"object_id",
)
content = models.JSONField(blank=True, default=dict)
created = models.DateTimeField(auto_now_add=True)
last_updated = models.DateTimeField(auto_now=True)

def get_content_as_list(self):
schema = dynamic_forms.Schema.parse_obj(self.model_binding.form.schema)

items = []
for field in schema.fields:
field_value = self.content.get(field.name)
field_kwargs = field.get_form_field_kwargs()
if "choices" in field_kwargs and field_value is not None:
choice_map = dict(field_kwargs["choices"])
if field.type == "multiple_choice":
value = [choice_map[i] for i in field_value]
else:
value = choice_map[field_value]
else:
value = field_value
if value:
label = field.get_verbose_name()
if isinstance(value, list) and field.type != "multiple_choice":
value = "|".join(map(str, value))
items.append((label, value))
return items


reversion.register(TagBinding)
reversion.register(ModelBinding)
reversion.register(UserDefinedForm)
21 changes: 21 additions & 0 deletions hawc/apps/udf/templates/udf/fragments/_udf_content.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<h3>User defined fields</h3>
<table class="table table-sm m-0 noborder">
munnsmunns marked this conversation as resolved.
Show resolved Hide resolved
<colgroup>
<col width="30%" />
<col width="70%" />
</colgroup>
<thead>
<tr class="tr-light list-border">
<th>Field</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{% for key, value in object.get_content_as_list %}
<tr class="tr-light">
<td>{{key}}</td>
<td>{{value}}</td>
</tr>
{% endfor %}
</tbody>
</table>
17 changes: 16 additions & 1 deletion hawc/apps/udf/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django.core.exceptions import PermissionDenied
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.urls import reverse_lazy
from django.utils.decorators import method_decorator
from django.views.generic import DetailView, ListView
Expand Down Expand Up @@ -200,3 +201,17 @@ class DeleteTagBindingView(BaseDelete):

def get_success_url(self):
return self.assessment.get_udf_list_url()


class UDFDetailMixin:
munnsmunns marked this conversation as resolved.
Show resolved Hide resolved
"""Mixin to add saved UDF contents to the context of a BaseDetail view."""

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
content_type = ContentType.objects.get_for_model(self.model)
try:
udf_binding = self.assessment.udf_bindings.get(content_type=content_type)
context["udf_content"] = udf_binding.saved_contents.get(object_id=self.object.pk)
except ObjectDoesNotExist:
context["udf_content"] = None
return context