Repository: comic/grand-challenge.org

Issue:
JSONEditor fails to load for component interface in display set form
**Describe the bug**
JSONEditor fails to load for component interface in display set form.

**To Reproduce**
Steps to reproduce the behavior:
1. Create a reader study containing a question where component interface field is set to an interface that is defined using JSON.
1. Go to the cases list view for that reader study.
1. Open a display set and click on edit.
1. The JSONEditor widget is not loaded for the interface field.
1. An error is shown in the console.

**Expected behavior**
JSONEditor widget loads correctly.

**Screenshots**

![image](https://user-images.githubusercontent.com/2001094/172585070-00d0d4bd-b22d-476c-9b11-9700d9b16a5c.png)

See the empty space below "Region-of-interest". The JSONEditor widget should show up there. As the error in the console indicates, the following files (JSONEditorWidget dependencies) are somehow not loaded:

https://github.com/comic/grand-challenge.org/blob/5a193d8970a66cfd53617262414da90b1ab0b2aa/app/grandchallenge/core/widgets.py#L17-L19


Discussion:
I think `{{ form.media }}` is not included anywhere, as the forms are loaded dynamically. I'm looking into a clean way to include it just once in the main list template and not for each form that is loaded.
Update: should have a fix, just waiting for mij terribly slow connection to pull in all the docker layers so I can test it.

Resolving patch:
diff --git a/app/grandchallenge/components/form_fields.py b/app/grandchallenge/components/form_fields.py
--- a/app/grandchallenge/components/form_fields.py
+++ b/app/grandchallenge/components/form_fields.py
@@ -29,6 +29,12 @@ def _join_with_br(a, b):
 
 
 class InterfaceFormField:
+    _possible_widgets = {
+        UserUploadMultipleWidget,
+        UserUploadSingleWidget,
+        JSONEditorWidget,
+    }
+
     def __init__(
         self,
         *,
diff --git a/app/grandchallenge/reader_studies/forms.py b/app/grandchallenge/reader_studies/forms.py
--- a/app/grandchallenge/reader_studies/forms.py
+++ b/app/grandchallenge/reader_studies/forms.py
@@ -1,5 +1,6 @@
 import csv
 import io
+import logging
 
 from crispy_forms.helper import FormHelper
 from crispy_forms.layout import (
@@ -56,6 +57,8 @@
 )
 from grandchallenge.subdomains.utils import reverse_lazy
 
+logger = logging.getLogger(__name__)
+
 READER_STUDY_HELP_TEXTS = {
     "title": "The title of this reader study.",
     "logo": "The logo for this reader study.",
@@ -477,6 +480,11 @@ def clean_ground_truth(self):
 
 
 class DisplaySetForm(Form):
+    _possible_widgets = {
+        Select,  # Default for ModelChoiceField
+        *InterfaceFormField._possible_widgets,
+    }
+
     def __init__(self, *args, instance=None, **kwargs):
         super().__init__(*args, **kwargs)
         if instance is not None:
@@ -485,6 +493,7 @@ def __init__(self, *args, instance=None, **kwargs):
                 civ,
             ) in instance.reader_study.values_for_interfaces.items():
                 val = instance.values.filter(interface__slug=slug).first()
+
                 if civ["kind"] in InterfaceKind.interface_type_json():
                     # Use the field/widget provided by InterfaceFormField,
                     # which includes proper validation
@@ -506,4 +515,5 @@ def __init__(self, *args, instance=None, **kwargs):
                         initial=val,
                         required=False,
                     )
+
             self.fields["order"] = IntegerField(initial=instance.order)
diff --git a/app/grandchallenge/reader_studies/views.py b/app/grandchallenge/reader_studies/views.py
--- a/app/grandchallenge/reader_studies/views.py
+++ b/app/grandchallenge/reader_studies/views.py
@@ -12,6 +12,7 @@
 )
 from django.db import transaction
 from django.db.models import Count, Q
+from django.forms import Media
 from django.forms.utils import ErrorList
 from django.http import (
     Http404,
@@ -376,13 +377,22 @@ class ReaderStudyDisplaySetList(
     ]
     text_align = "left"
     default_sort_order = "asc"
+    form_class = DisplaySetForm
 
     @cached_property
     def reader_study(self):
         return get_object_or_404(ReaderStudy, slug=self.kwargs["slug"])
 
     def render_row(self, *, object_, page_context):
-        form = DisplaySetForm(instance=object_)
+        form = self.form_class(instance=object_)
+
+        all_media = page_context["form_media"] + form.media
+        if all_media.render() != page_context["form_media"].render():
+            raise RuntimeError(
+                "Media is missing for this form, ensure that all widgets "
+                "have been declared in _possible_widgets on the forms class."
+            )
+
         return render_to_string(
             self.row_template,
             context={**page_context, "object": object_, "form": form},
@@ -393,8 +403,14 @@ def get_permission_object(self):
 
     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
+
+        media = Media()
+        for widget in self.form_class._possible_widgets:
+            media = media + widget().media
+
         context.update(
             {
+                "form_media": media,
                 "reader_study": self.reader_study,
                 "reader_study_display_set_view_feature": settings.READER_STUDY_DISPLAY_SET_VIEW_FEATURE,
             }
@@ -1281,6 +1297,11 @@ class DisplaySetUpdate(
     )
     raise_exception = True
 
+    def get_form_kwargs(self):
+        kwargs = super().get_form_kwargs()
+        kwargs.update({"auto_id": f"id-{self.kwargs['pk']}-%s"})
+        return kwargs
+
     def get_success_url(self):
         return reverse(
             "reader-studies:display-set-detail",


Test patch: