Repository: comic/grand-challenge.org

Issue:
Add and update display set widgets incorrectly handle all interfaces as required.
**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:
1. Go to a Reader study (for example https://grand-challenge.org/reader-studies/cirruscoreweb-release-display-sets)-Cases-Add Case (single). The reader study needs to have at least 1 case with multiple interfaces.
2. Select an existing image for interface Generic-medical-image
3. Hit Save
4. Screen shows Unexpected error
5. Sentry event: https://grand-challenge.sentry.io/issues/3926657067/?project=303639&query=is%3Aunresolved&referrer=issue-stream&statsPeriod=24h

**Expected behavior**
A clear and concise description of what you expected to happen.
It should be possible to add a display set that does not have values for all interfaces.

**Screenshots**
If applicable, add screenshots to help explain your problem.
![image](https://user-images.githubusercontent.com/51329283/217807790-30c8a0db-f737-4366-9161-0ae7c5efcb79.png)


**Desktop (please complete the following information):**
 - OS: [e.g. iOS]
 - Browser [e.g. chrome, safari]
 - Version [e.g. 22]

**Smartphone (please complete the following information):**
 - Device: [e.g. iPhone6]
 - OS: [e.g. iOS8.1]
 - Browser [e.g. stock browser, safari]
 - Version [e.g. 22]

**Additional context**
Add any other context about the problem here.



Discussion:

Resolving patch:
diff --git a/app/grandchallenge/cases/widgets.py b/app/grandchallenge/cases/widgets.py
--- a/app/grandchallenge/cases/widgets.py
+++ b/app/grandchallenge/cases/widgets.py
@@ -8,7 +8,6 @@
     MultiWidget,
 )
 from django.forms.widgets import ChoiceWidget
-from django.utils.datastructures import MultiValueDictKeyError
 
 from grandchallenge.cases.models import Image
 from grandchallenge.uploads.widgets import UserUploadMultipleWidget
@@ -76,11 +75,11 @@ def decompress(self, value):
     def value_from_datadict(self, data, files, name):
         try:
             value = data[name]
-        except MultiValueDictKeyError:
+        except KeyError:
+            # this happens if the data comes from the DS create / update form
             try:
-                # this happens if the data comes from the DS update form
                 value = data[f"WidgetChoice-{name}"]
-            except MultiValueDictKeyError:
+            except KeyError:
                 value = None
         if value:
             if value in WidgetChoices.names:
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
@@ -615,6 +615,7 @@ def _get_image_field(self, *, interface, values, current_value):
             widget=FlexibleImageWidget(
                 user=self.user, current_value=current_value
             ),
+            required=False,
         )
 
     def _get_file_field(self, *, interface, values, current_value):


Test patch:
diff --git a/app/tests/cases_tests/test_widget.py b/app/tests/cases_tests/test_widget.py
--- a/app/tests/cases_tests/test_widget.py
+++ b/app/tests/cases_tests/test_widget.py
@@ -24,6 +24,13 @@ def test_flexible_image_field_validation():
         image_queryset=get_objects_for_user(user, "cases.view_image"),
         upload_queryset=UserUpload.objects.filter(creator=user).all(),
     )
+    parsed_value_for_empty_data = field.widget.value_from_datadict(
+        data={}, name=ci.slug, files={}
+    )
+    decompressed_value_for_missing_value = field.widget.decompress(value=None)
+    assert not parsed_value_for_empty_data
+    assert decompressed_value_for_missing_value == [None, None]
+
     parsed_value_for_image_with_permission = field.widget.value_from_datadict(
         data={ci.slug: im1.pk}, name=ci.slug, files={}
     )
diff --git a/app/tests/reader_studies_tests/test_forms.py b/app/tests/reader_studies_tests/test_forms.py
--- a/app/tests/reader_studies_tests/test_forms.py
+++ b/app/tests/reader_studies_tests/test_forms.py
@@ -1111,6 +1111,33 @@ def test_display_set_update_form(form_class, file_widget):
     ]
 
 
+@pytest.mark.parametrize(
+    "form_class",
+    (
+        DisplaySetCreateForm,
+        DisplaySetUpdateForm,
+    ),
+)
+@pytest.mark.django_db
+def test_display_set_form_interface_fields_not_required(form_class):
+    rs = ReaderStudyFactory()
+    user = UserFactory()
+    rs.add_editor(user)
+    ci_img = ComponentInterfaceFactory(kind="IMG")
+    ci_file = ComponentInterfaceFactory(kind="JSON", store_in_database=False)
+    ci_str = ComponentInterfaceFactory(kind="STR")
+    for ci in [ci_img, ci_file, ci_str]:
+        civ = ComponentInterfaceValueFactory(interface=ci)
+        ds = DisplaySetFactory(reader_study=rs)
+        ds.values.add(civ)
+
+    instance = None if form_class == DisplaySetCreateForm else ds
+    form = form_class(user=user, instance=instance, reader_study=rs)
+    for name, field in form.fields.items():
+        if not name == "order":
+            assert not field.required
+
+
 @pytest.mark.django_db
 def test_display_set_update_form_image_field_queryset_filters():
     rs = ReaderStudyFactory()