Skip to content

Commit 5f11516

Browse files
authored
Avoid error when creating new item with empty value for a linked socket (#4373)
Close #4371 Creating new items (e.g. display sets or archive items) with an undefined value for one or more sockets would raise an error since #4333. This is a common use case and is allowed. Also add `__repr__` to `CIVData` for easier debugging.
1 parent c97b785 commit 5f11516

3 files changed

Lines changed: 107 additions & 8 deletions

File tree

app/grandchallenge/components/models.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2262,6 +2262,12 @@ def file_civ(self):
22622262
def dicom_upload_with_name(self):
22632263
return self._dicom_upload_with_name
22642264

2265+
def __repr__(self):
2266+
return f"CIVData(interface_slug={self._interface_slug!r}, value={self._initial_value!r})"
2267+
2268+
def __str__(self):
2269+
return f"CIVData: {self.__dict__}"
2270+
22652271
def __init__(self, *, interface_slug, value):
22662272
self._interface_slug = interface_slug
22672273
self._initial_value = value
@@ -2412,13 +2418,13 @@ def validate_civ_data_objects_and_execute_linked_task(
24122418
self, *, civ_data_objects, user, linked_task=None
24132419
):
24142420
for civ_data in civ_data_objects:
2415-
self.create_civ(
2421+
self._update_civ(
24162422
civ_data=civ_data,
24172423
user=user,
24182424
linked_task=linked_task,
24192425
)
24202426

2421-
def create_civ(self, *, civ_data, user=None, linked_task=None):
2427+
def _update_civ(self, *, civ_data, user=None, linked_task=None):
24222428
if not self.is_editable:
24232429
raise CIVNotEditableException(
24242430
f"{self} is not editable. CIVs cannot be added or removed from it.",
@@ -2442,15 +2448,15 @@ def create_civ(self, *, civ_data, user=None, linked_task=None):
24422448
)
24432449

24442450
if ci.super_kind == ci.SuperKind.VALUE:
2445-
return self._create_civ_for_value(
2451+
return self._update_civ_for_value(
24462452
ci=ci,
24472453
current_civ=current_civ,
24482454
new_value=civ_data.value,
24492455
user=user,
24502456
linked_task=linked_task,
24512457
)
24522458
elif ci.super_kind == ci.SuperKind.IMAGE:
2453-
return self._create_civ_for_image(
2459+
return self._update_civ_for_image(
24542460
ci=ci,
24552461
current_civ=current_civ,
24562462
user=user,
@@ -2461,7 +2467,7 @@ def create_civ(self, *, civ_data, user=None, linked_task=None):
24612467
linked_task=linked_task,
24622468
)
24632469
elif ci.super_kind == ci.SuperKind.FILE:
2464-
return self._create_civ_for_file(
2470+
return self._update_civ_for_file(
24652471
ci=ci,
24662472
current_civ=current_civ,
24672473
file_civ=civ_data.file_civ,
@@ -2473,7 +2479,7 @@ def create_civ(self, *, civ_data, user=None, linked_task=None):
24732479
f"Unknown interface super kind: {ci.super_kind}"
24742480
)
24752481

2476-
def _create_civ_for_value(
2482+
def _update_civ_for_value(
24772483
self, *, ci, current_civ, new_value, user, linked_task=None
24782484
):
24792485
current_value = current_civ.value if current_civ else None
@@ -2509,7 +2515,7 @@ def _create_civ_for_value(
25092515
if linked_task is not None:
25102516
on_commit(signature(linked_task).apply_async)
25112517

2512-
def _create_civ_for_image( # noqa: C901
2518+
def _update_civ_for_image( # noqa: C901
25132519
self,
25142520
*,
25152521
ci,
@@ -2633,10 +2639,13 @@ def _create_civ_for_image( # noqa: C901
26332639
kwargs={"dicom_imageset_upload_pk": upload.pk}
26342640
).apply_async
26352641
)
2642+
elif current_civ is None:
2643+
# Nothing to do
2644+
pass
26362645
else:
26372646
raise NotImplementedError
26382647

2639-
def _create_civ_for_file(
2648+
def _update_civ_for_file(
26402649
self,
26412650
*,
26422651
ci,

app/tests/archives_tests/test_views.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44

55
import pytest
66
from django.contrib.auth.models import Group
7+
from django.core.exceptions import ObjectDoesNotExist
78
from django.core.files.base import ContentFile
89
from guardian.shortcuts import assign_perm, remove_perm
910
from requests import put
1011

1112
from grandchallenge.archives.models import ArchiveItem
1213
from grandchallenge.archives.views import ArchiveItemsList
14+
from grandchallenge.cases.widgets import ImageWidgetChoices
1315
from grandchallenge.components.form_fields import INTERFACE_FORM_FIELD_PREFIX
1416
from grandchallenge.components.models import (
1517
ComponentInterface,
@@ -1251,6 +1253,49 @@ def test_archive_item_create_view(
12511253
assert not UserUpload.objects.filter(pk=upload.pk).exists()
12521254

12531255

1256+
@pytest.mark.django_db
1257+
def test_archive_item_create_view_with_empty_value(
1258+
client, settings, django_capture_on_commit_callbacks
1259+
):
1260+
settings.task_eager_propagates = (True,)
1261+
settings.task_always_eager = (True,)
1262+
1263+
archive = ArchiveFactory()
1264+
editor = UserFactory()
1265+
archive.add_editor(editor)
1266+
existing_archive_item = ArchiveItemFactory(archive=archive)
1267+
ci_str = ComponentInterfaceFactory(kind=InterfaceKindChoices.STRING)
1268+
ci_img = ComponentInterfaceFactory(kind=InterfaceKindChoices.PANIMG_IMAGE)
1269+
civ_str = ci_str.create_instance(value="foo")
1270+
civ_img = ci_img.create_instance(image=ImageFactory())
1271+
existing_archive_item.values.set([civ_str, civ_img])
1272+
1273+
with django_capture_on_commit_callbacks(execute=True):
1274+
response = get_view_for_user(
1275+
viewname="archives:item-create",
1276+
client=client,
1277+
reverse_kwargs={"slug": archive.slug},
1278+
data={
1279+
**get_interface_form_data(
1280+
interface_slug=ci_str.slug, data="bar"
1281+
),
1282+
f"widget-choice-{INTERFACE_FORM_FIELD_PREFIX}{ci_img.slug}": ImageWidgetChoices.UNDEFINED.name,
1283+
"title": "archive-item title",
1284+
},
1285+
user=editor,
1286+
method=client.post,
1287+
)
1288+
1289+
assert response.status_code == 302
1290+
assert ArchiveItem.objects.count() == 2
1291+
new_archive_item = ArchiveItem.objects.order_by("created").last()
1292+
assert new_archive_item.title == "archive-item title"
1293+
assert new_archive_item.values.count() == 1
1294+
assert new_archive_item.values.get(interface=ci_str).value == "bar"
1295+
with pytest.raises(ObjectDoesNotExist):
1296+
new_archive_item.values.get(interface=ci_img)
1297+
1298+
12541299
@pytest.mark.django_db
12551300
def test_archive_item_bulk_delete_permissions(client):
12561301
user, editor = UserFactory.create_batch(2)

app/tests/reader_studies_tests/test_views.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import io
22

33
import pytest
4+
from django.core.exceptions import ObjectDoesNotExist
45
from django.forms import JSONField
56
from django.test import override_settings
67
from guardian.shortcuts import assign_perm
@@ -722,6 +723,50 @@ def test_add_display_set_to_reader_study(
722723
assert ds.values.get(interface=ci_json).file.read() == b'{"foo": "bar"}'
723724

724725

726+
@pytest.mark.django_db
727+
def test_add_display_set_to_reader_study_with_empty_value(
728+
client, settings, django_capture_on_commit_callbacks
729+
):
730+
settings.task_eager_propagates = (True,)
731+
settings.task_always_eager = (True,)
732+
733+
editor = UserFactory()
734+
reader_study = ReaderStudyFactory()
735+
existing_display_set = DisplaySetFactory(reader_study=reader_study)
736+
reader_study.add_editor(editor)
737+
ci_str = ComponentInterfaceFactory(kind=InterfaceKindChoices.STRING)
738+
ci_img = ComponentInterfaceFactory(kind=InterfaceKindChoices.PANIMG_IMAGE)
739+
civ_str = ci_str.create_instance(value="foo")
740+
civ_img = ci_img.create_instance(image=ImageFactory())
741+
existing_display_set.values.set([civ_str, civ_img])
742+
743+
assert DisplaySet.objects.count() == 1
744+
745+
with django_capture_on_commit_callbacks(execute=True):
746+
response = get_view_for_user(
747+
viewname="reader-studies:display-set-create",
748+
client=client,
749+
reverse_kwargs={"slug": reader_study.slug},
750+
data={
751+
**get_interface_form_data(
752+
interface_slug=ci_str.slug, data="bar"
753+
),
754+
f"widget-choice-{INTERFACE_FORM_FIELD_PREFIX}{ci_img.slug}": ImageWidgetChoices.UNDEFINED.name,
755+
"order": 11,
756+
},
757+
user=editor,
758+
method=client.post,
759+
)
760+
761+
assert response.status_code == 302
762+
assert DisplaySet.objects.count() == 2
763+
new_display_set = DisplaySet.objects.order_by("created").last()
764+
assert new_display_set.values.count() == 1
765+
assert new_display_set.values.get(interface=ci_str).value == "bar"
766+
with pytest.raises(ObjectDoesNotExist):
767+
new_display_set.values.get(interface=ci_img)
768+
769+
725770
@pytest.mark.django_db
726771
def test_add_display_set_update_when_disabled(client):
727772
editor = UserFactory()

0 commit comments

Comments
 (0)