Skip to content

Commit a706b47

Browse files
authored
Avoid passing empty string in flexible fields data validation (#4528)
Remove the hidden input fields with the interface form field prefix to avoid passing these empty values. These fields were used for easy parsing in the `MultipleCIVForm`; however, they were not removed in `InterfaceFormFieldsMixin.clean` if the choice was `UNDEFINED`, effectively passing an empty string as the desired value for the socket. We now recognise the flexible source choice field as an interface field, so we do not need the hidden fields anymore.
1 parent 1f0a53b commit a706b47

7 files changed

Lines changed: 66 additions & 74 deletions

File tree

app/grandchallenge/components/forms.py

Lines changed: 23 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
from django.contrib.auth import get_user_model
1111
from django.forms import (
1212
CheckboxSelectMultiple,
13-
Field,
1413
Form,
1514
HiddenInput,
1615
ModelChoiceField,
@@ -222,11 +221,6 @@ def get_fields_for_interface(
222221
required=False,
223222
bound_field_class=BoundFieldWithDNoneClass,
224223
),
225-
# Add hidden input field for parsing when rendering
226-
# dynamically added fields.
227-
prefixed_interface_slug: Field(
228-
required=False, widget=HiddenInput()
229-
),
230224
}
231225
elif interface.super_kind == interface.SuperKind.FILE:
232226
return {
@@ -254,11 +248,6 @@ def get_fields_for_interface(
254248
required=False,
255249
bound_field_class=BoundFieldWithDNoneClass,
256250
),
257-
# Add hidden input field for parsing when rendering
258-
# dynamically added fields.
259-
prefixed_interface_slug: Field(
260-
required=False, widget=HiddenInput()
261-
),
262251
}
263252
elif interface.super_kind == interface.SuperKind.VALUE:
264253
return {
@@ -448,36 +437,33 @@ def __init__(self, *args, instance, base_obj, user, **kwargs): # noqa C901
448437
)
449438
)
450439

451-
# Add fields for dynamically added new interfaces
452-
for slug in self.data.keys():
453-
interface_slug = self.parse_slug(slug=slug)
454-
455-
if (
456-
interface_slug
457-
and slug not in self.fields.keys()
458-
and ComponentInterface.objects.filter(
459-
slug=interface_slug
460-
).exists()
461-
):
462-
interface = ComponentInterface.objects.filter(
463-
slug=interface_slug
464-
).get()
465-
466-
self.fields.update(
467-
self.get_fields_for_interface(
468-
interface=interface,
469-
user=self.user,
470-
required=False,
471-
)
440+
for interface in self.get_dynamically_added_interfaces():
441+
self.fields.update(
442+
self.get_fields_for_interface(
443+
interface=interface,
444+
user=self.user,
445+
required=False,
472446
)
447+
)
473448

474-
@staticmethod
475-
def parse_slug(*, slug):
476-
if not slug.startswith(INTERFACE_FORM_FIELD_PREFIX):
477-
return None
449+
def get_dynamically_added_interfaces(self):
450+
new_interface_slugs = set()
451+
for field_name in self.data.keys():
452+
interface_slug = self.get_interface_slug(field_name=field_name)
478453

479-
interface_slug = slug[len(INTERFACE_FORM_FIELD_PREFIX) :]
454+
if interface_slug and field_name not in self.fields.keys():
455+
new_interface_slugs.add(interface_slug)
480456

457+
return ComponentInterface.objects.filter(slug__in=new_interface_slugs)
458+
459+
@staticmethod
460+
def get_interface_slug(*, field_name):
461+
if field_name.startswith(INTERFACE_FORM_FIELD_PREFIX):
462+
interface_slug = field_name[len(INTERFACE_FORM_FIELD_PREFIX) :]
463+
elif field_name.startswith(FlexibleWidgetPrefixes.CHOICE):
464+
interface_slug = field_name[len(FlexibleWidgetPrefixes.CHOICE) :]
465+
else:
466+
interface_slug = None
481467
return interface_slug
482468

483469
def clean(self):

app/tests/algorithms_tests/test_forms.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,6 @@ def try_create_algorithm():
224224
f'name="{FlexibleWidgetPrefixes.UPLOAD}some-overlay"',
225225
f'name="{FlexibleWidgetPrefixes.SEARCH}some-overlay_{SearchWidgetSuffixes.INPUT}"',
226226
f'name="{FlexibleWidgetPrefixes.SEARCH}some-overlay_{SearchWidgetSuffixes.CHOICE}"',
227-
f'name="{INTERFACE_FORM_FIELD_PREFIX}some-overlay"',
228227
],
229228
),
230229
(
@@ -237,7 +236,6 @@ def try_create_algorithm():
237236
f'name="{FlexibleWidgetPrefixes.UPLOAD}some-medical-image"',
238237
f'name="{FlexibleWidgetPrefixes.SEARCH}some-medical-image_{SearchWidgetSuffixes.INPUT}"',
239238
f'name="{FlexibleWidgetPrefixes.SEARCH}some-medical-image_{SearchWidgetSuffixes.CHOICE}"',
240-
f'name="{INTERFACE_FORM_FIELD_PREFIX}some-medical-image"',
241239
],
242240
),
243241
(
@@ -251,7 +249,6 @@ def try_create_algorithm():
251249
f'name="{FlexibleWidgetPrefixes.UPLOAD}some-medical-image_{DICOMUploadWidgetSuffixes.UPLOADS}"',
252250
f'name="{FlexibleWidgetPrefixes.SEARCH}some-medical-image_{SearchWidgetSuffixes.INPUT}"',
253251
f'name="{FlexibleWidgetPrefixes.SEARCH}some-medical-image_{SearchWidgetSuffixes.CHOICE}"',
254-
f'name="{INTERFACE_FORM_FIELD_PREFIX}some-medical-image"',
255252
],
256253
),
257254
(
@@ -386,7 +383,6 @@ def try_create_algorithm():
386383
f'name="{FlexibleWidgetPrefixes.UPLOAD}anything"',
387384
f'name="{FlexibleWidgetPrefixes.SEARCH}anything_{SearchWidgetSuffixes.INPUT}"',
388385
f'name="{FlexibleWidgetPrefixes.SEARCH}anything_{SearchWidgetSuffixes.CHOICE}"',
389-
f'name="{INTERFACE_FORM_FIELD_PREFIX}anything"',
390386
],
391387
),
392388
),

app/tests/archives_tests/test_views.py

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from grandchallenge.subdomains.utils import reverse
2727
from grandchallenge.uploads.models import UserUpload
2828
from tests.algorithms_tests.factories import AlgorithmInterfaceFactory
29+
from tests.algorithms_tests.test_forms import extract_form_data_from_response
2930
from tests.archives_tests.factories import (
3031
ArchiveFactory,
3132
ArchiveItemFactory,
@@ -1182,12 +1183,11 @@ def test_archive_item_create_view(
11821183
client=client,
11831184
user=editor,
11841185
)
1185-
assert len(response.context["form"].fields) == 6
11861186
assert response.context["form"].fields[
11871187
f"{INTERFACE_FORM_FIELD_PREFIX}{ci_str.slug}"
11881188
]
11891189
assert response.context["form"].fields[
1190-
f"{INTERFACE_FORM_FIELD_PREFIX}{ci_img.slug}"
1190+
f"{FlexibleWidgetPrefixes.CHOICE}{ci_img.slug}"
11911191
]
11921192
assert response.context["form"].fields["title"]
11931193

@@ -1261,22 +1261,37 @@ def test_archive_item_create_view_with_empty_value(
12611261
existing_archive_item = ArchiveItemFactory(archive=archive)
12621262
ci_str = ComponentInterfaceFactory(kind=InterfaceKindChoices.STRING)
12631263
ci_img = ComponentInterfaceFactory(kind=InterfaceKindChoices.PANIMG_IMAGE)
1264+
ci_file = ComponentInterfaceFactory(
1265+
kind=InterfaceKindChoices.ANY, store_in_database=False
1266+
)
12641267
civ_str = ci_str.create_instance(value="foo")
12651268
civ_img = ci_img.create_instance(image=ImageFactory())
1266-
existing_archive_item.values.set([civ_str, civ_img])
1269+
civ_file = ComponentInterfaceValueFactory(interface=ci_file)
1270+
existing_archive_item.values.set([civ_str, civ_img, civ_file])
1271+
1272+
response = get_view_for_user(
1273+
viewname="archives:item-create",
1274+
client=client,
1275+
reverse_kwargs={"slug": archive.slug},
1276+
user=editor,
1277+
method=client.get,
1278+
)
1279+
data = extract_form_data_from_response(response)
1280+
data.update(
1281+
{
1282+
**get_interface_form_data(interface_slug=ci_str.slug, data="bar"),
1283+
f"{FlexibleWidgetPrefixes.CHOICE}{ci_img.slug}": ImageSourceChoices.UNDEFINED.value,
1284+
f"{FlexibleWidgetPrefixes.CHOICE}{ci_file.slug}": ImageSourceChoices.UNDEFINED.value,
1285+
"title": "archive-item title",
1286+
}
1287+
)
12671288

12681289
with django_capture_on_commit_callbacks(execute=True):
12691290
response = get_view_for_user(
12701291
viewname="archives:item-create",
12711292
client=client,
12721293
reverse_kwargs={"slug": archive.slug},
1273-
data={
1274-
**get_interface_form_data(
1275-
interface_slug=ci_str.slug, data="bar"
1276-
),
1277-
f"{FlexibleWidgetPrefixes.CHOICE}{ci_img.slug}": ImageSourceChoices.UNDEFINED.value,
1278-
"title": "archive-item title",
1279-
},
1294+
data=data,
12801295
user=editor,
12811296
method=client.post,
12821297
)
@@ -1289,6 +1304,8 @@ def test_archive_item_create_view_with_empty_value(
12891304
assert new_archive_item.values.get(interface=ci_str).value == "bar"
12901305
with pytest.raises(ObjectDoesNotExist):
12911306
new_archive_item.values.get(interface=ci_img)
1307+
with pytest.raises(ObjectDoesNotExist):
1308+
new_archive_item.values.get(interface=ci_file)
12921309

12931310

12941311
@pytest.mark.django_db

app/tests/cases_tests/test_forms.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from grandchallenge.cases.models import RawImageUploadSession
55
from grandchallenge.components.forms import (
66
INTERFACE_FORM_FIELD_PREFIX,
7+
FlexibleWidgetPrefixes,
78
MultipleCIVForm,
89
)
910
from grandchallenge.components.models import ComponentInterface
@@ -79,10 +80,15 @@ def test_upload_some_images(
7980
assert response.status_code == 403
8081

8182

82-
def test_parse_slug():
83-
assert (
84-
MultipleCIVForm.parse_slug(
85-
slug=f"{INTERFACE_FORM_FIELD_PREFIX}foo-bar"
86-
)
87-
== "foo-bar"
88-
)
83+
@pytest.mark.parametrize(
84+
"field_name, slug",
85+
(
86+
(f"{INTERFACE_FORM_FIELD_PREFIX}foo-bar", "foo-bar"),
87+
(f"{FlexibleWidgetPrefixes.CHOICE}foo-bar", "foo-bar"),
88+
# Ignore because there should be a choice field for the interface
89+
(f"{FlexibleWidgetPrefixes.SEARCH}foo-bar", None),
90+
("foo-bar", None), # not an interface field
91+
),
92+
)
93+
def test_get_interface_slug(field_name, slug):
94+
assert MultipleCIVForm.get_interface_slug(field_name=field_name) == slug

app/tests/conftest.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -530,7 +530,6 @@ def get_interface_form_data(
530530
form_data = {
531531
f"{FlexibleWidgetPrefixes.CHOICE}{interface_slug}": SourceChoices.SEARCH.value,
532532
f"{FlexibleWidgetPrefixes.SEARCH}{interface_slug}_{SearchWidgetSuffixes.CHOICE}": data,
533-
f"{INTERFACE_FORM_FIELD_PREFIX}{interface_slug}": "",
534533
}
535534
else:
536535
if ci.is_dicom_image_kind:
@@ -542,29 +541,27 @@ def get_interface_form_data(
542541
f"{FlexibleWidgetPrefixes.UPLOAD}{interface_slug}_{DICOMUploadWidgetSuffixes.UPLOADS}": data[
543542
1
544543
],
545-
f"{INTERFACE_FORM_FIELD_PREFIX}{interface_slug}": "",
546544
}
547545
else:
548546
form_data = {
549547
f"{FlexibleWidgetPrefixes.CHOICE}{interface_slug}": SourceChoices.UPLOAD.value,
550548
f"{FlexibleWidgetPrefixes.UPLOAD}{interface_slug}": data,
551-
f"{INTERFACE_FORM_FIELD_PREFIX}{interface_slug}": "",
552549
}
553550
elif ci.super_kind == ci.SuperKind.FILE:
554551
if existing_data:
555552
form_data = {
556553
f"{FlexibleWidgetPrefixes.CHOICE}{interface_slug}": SourceChoices.SEARCH.value,
557554
f"{FlexibleWidgetPrefixes.SEARCH}{interface_slug}_{SearchWidgetSuffixes.CHOICE}": data,
558-
f"{INTERFACE_FORM_FIELD_PREFIX}{interface_slug}": "",
559555
}
560556
else:
561557
form_data = {
562558
f"{FlexibleWidgetPrefixes.CHOICE}{interface_slug}": SourceChoices.UPLOAD.value,
563559
f"{FlexibleWidgetPrefixes.UPLOAD}{interface_slug}": data,
564-
f"{INTERFACE_FORM_FIELD_PREFIX}{interface_slug}": "",
565560
}
566-
else:
561+
elif ci.super_kind == ci.SuperKind.VALUE:
567562
form_data = {f"{INTERFACE_FORM_FIELD_PREFIX}{interface_slug}": data}
563+
else:
564+
raise NotImplementedError
568565

569566
return form_data
570567

app/tests/evaluation_tests/test_forms.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,10 @@
44
from django.core.validators import MaxValueValidator, MinValueValidator
55
from django.forms import (
66
CharField,
7-
HiddenInput,
87
ModelChoiceField,
98
ModelMultipleChoiceField,
109
TextInput,
1110
)
12-
from django.forms.fields import Field
1311
from factory.django import ImageField
1412

1513
from grandchallenge.algorithms.forms import (
@@ -1350,7 +1348,6 @@ def test_additional_inputs_on_submission_form():
13501348
ImageSearchMultiField,
13511349
ImageSearchMultiWidget,
13521350
),
1353-
f"{INTERFACE_FORM_FIELD_PREFIX}{ci_img.slug}": (Field, HiddenInput),
13541351
f"{FlexibleWidgetPrefixes.CHOICE}{ci_dicom.slug}": (
13551352
ImageSourceChoiceField,
13561353
SourceSelect,
@@ -1363,7 +1360,6 @@ def test_additional_inputs_on_submission_form():
13631360
ImageSearchMultiField,
13641361
ImageSearchMultiWidget,
13651362
),
1366-
f"{INTERFACE_FORM_FIELD_PREFIX}{ci_dicom.slug}": (Field, HiddenInput),
13671363
f"{INTERFACE_FORM_FIELD_PREFIX}{ci_str.slug}": (CharField, TextInput),
13681364
f"{FlexibleWidgetPrefixes.CHOICE}{ci_file.slug}": (
13691365
FileSourceChoiceField,
@@ -1377,7 +1373,6 @@ def test_additional_inputs_on_submission_form():
13771373
FileSearchMultiField,
13781374
FileSearchMultiWidget,
13791375
),
1380-
f"{INTERFACE_FORM_FIELD_PREFIX}{ci_file.slug}": (Field, HiddenInput),
13811376
}
13821377

13831378
form = SubmissionForm(

app/tests/reader_studies_tests/test_forms.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from django.db.models import BLANK_CHOICE_DASH
1010
from django.forms import (
1111
CharField,
12-
Field,
1312
HiddenInput,
1413
JSONField,
1514
ModelChoiceField,
@@ -1478,7 +1477,6 @@ def test_display_set_update_form(form_class):
14781477
FileSearchMultiField,
14791478
FileSearchMultiWidget,
14801479
),
1481-
f"{INTERFACE_FORM_FIELD_PREFIX}{ci_file.slug}": (Field, HiddenInput),
14821480
}
14831481

14841482
form = form_class(user=user, instance=instance, base_obj=rs)
@@ -1680,7 +1678,6 @@ def test_display_set_add_interface_form():
16801678
FileSearchMultiField,
16811679
FileSearchMultiWidget,
16821680
),
1683-
f"{INTERFACE_FORM_FIELD_PREFIX}{ci_file.slug}": (Field, HiddenInput),
16841681
}
16851682

16861683
assert form.fields.keys() == set(expected_fields).union({"interface"})
@@ -1729,7 +1726,6 @@ def test_display_set_add_interface_form():
17291726
ImageSearchMultiField,
17301727
ImageSearchMultiWidget,
17311728
),
1732-
f"{INTERFACE_FORM_FIELD_PREFIX}{ci_image.slug}": (Field, HiddenInput),
17331729
}
17341730

17351731
assert form.fields.keys() == set(expected_fields).union({"interface"})
@@ -1759,7 +1755,6 @@ def test_display_set_add_interface_form():
17591755
ImageSearchMultiField,
17601756
ImageSearchMultiWidget,
17611757
),
1762-
f"{INTERFACE_FORM_FIELD_PREFIX}{ci_dicom.slug}": (Field, HiddenInput),
17631758
}
17641759

17651760
assert form.fields.keys() == set(expected_fields).union({"interface"})

0 commit comments

Comments
 (0)