Repository: comic/grand-challenge.org

Issue:
Reader study copying does not work with display sets.
It looks like you end up with images set on the reader study again, and no display sets present.

https://github.com/comic/grand-challenge.org/blob/c68d359bcd23e45becdc1899e4e299859ed3e3ee/app/grandchallenge/reader_studies/views.py#L603


Discussion:

Resolving patch:
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
@@ -247,8 +247,9 @@ class Meta(ReaderStudyCreateForm.Meta):
 class ReaderStudyCopyForm(Form):
     title = CharField(required=True)
     description = CharField(required=False, widget=Textarea())
-    copy_images = BooleanField(required=False, initial=True)
-    copy_hanging_list = BooleanField(required=False, initial=True)
+    copy_display_sets = BooleanField(required=False, initial=True)
+    copy_hanging_protocol = BooleanField(required=False, initial=True)
+    copy_view_content = BooleanField(required=False, initial=True)
     copy_case_text = BooleanField(required=False, initial=True)
     copy_questions = BooleanField(required=False, initial=True)
     copy_readers = BooleanField(required=False, initial=True)
@@ -259,16 +260,6 @@ def __init__(self, *args, **kwargs):
         self.helper = FormHelper(self)
         self.helper.layout.append(Submit("save", "Copy"))
 
-    def clean(self, *args, **kwargs):
-        if (
-            self.cleaned_data["copy_hanging_list"]
-            or self.cleaned_data["copy_case_text"]
-        ) and not self.cleaned_data["copy_images"]:
-            self.add_error(
-                error="Hanging list and case text can only be copied if the images are copied as well",
-                field=None,
-            )
-
 
 class QuestionForm(SaveFormInitMixin, ModelForm):
     def __init__(self, *args, **kwargs):
diff --git a/app/grandchallenge/reader_studies/tasks.py b/app/grandchallenge/reader_studies/tasks.py
--- a/app/grandchallenge/reader_studies/tasks.py
+++ b/app/grandchallenge/reader_studies/tasks.py
@@ -124,3 +124,13 @@ def add_image_to_answer(*, upload_session_pk, answer_pk):
         add_image(answer, image)
     else:
         raise ValueError("Upload session for answer does not match")
+
+
+@shared_task(**settings.CELERY_TASK_DECORATOR_KWARGS["acks-late-2xlarge"])
+def copy_reader_study_display_sets(*, orig_pk, new_pk):
+    orig = ReaderStudy.objects.get(pk=orig_pk)
+    new = ReaderStudy.objects.get(pk=new_pk)
+    with transaction.atomic():
+        for ds in orig.display_sets.all():
+            new_ds = DisplaySet.objects.create(reader_study=new)
+            new_ds.values.set(ds.values.all())
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
@@ -97,6 +97,7 @@
 )
 from grandchallenge.reader_studies.tasks import (
     add_images_to_reader_study,
+    copy_reader_study_display_sets,
     create_display_sets_for_upload_session,
 )
 from grandchallenge.subdomains.utils import reverse
@@ -599,10 +600,10 @@ def form_valid(self, form):  # noqa: C901
             },
         )
         rs.add_editor(self.request.user)
-        if form.cleaned_data["copy_images"]:
-            rs.images.set(reader_study.images.all())
-        if form.cleaned_data["copy_hanging_list"]:
-            rs.hanging_list = reader_study.hanging_list
+        if form.cleaned_data["copy_view_content"]:
+            rs.view_content = reader_study.view_content
+        if form.cleaned_data["copy_hanging_protocol"]:
+            rs.hanging_protocol = reader_study.hanging_protocol
         if form.cleaned_data["copy_case_text"]:
             rs.case_text = reader_study.case_text
         if form.cleaned_data["copy_readers"]:
@@ -630,6 +631,20 @@ def form_valid(self, form):  # noqa: C901
                     )
         rs.save()
         self.reader_study = rs
+        if form.cleaned_data["copy_display_sets"]:
+            transaction.on_commit(
+                lambda: copy_reader_study_display_sets.apply_async(
+                    kwargs={
+                        "orig_pk": str(reader_study.pk),
+                        "new_pk": str(rs.pk),
+                    }
+                )
+            )
+            messages.add_message(
+                self.request,
+                messages.INFO,
+                "Display sets will be copied asynchronously.",
+            )
         return super().form_valid(form)
 
     def get_success_url(self):


Test patch:
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
@@ -5,6 +5,7 @@
 import pytest
 from actstream.actions import is_following
 from django.contrib.auth.models import Permission
+from django_capture_on_commit_callbacks import capture_on_commit_callbacks
 
 from grandchallenge.core.utils.access_requests import (
     AccessRequestHandlingOptions,
@@ -12,6 +13,7 @@
 from grandchallenge.reader_studies.models import Answer, Question, ReaderStudy
 from tests.components_tests.factories import ComponentInterfaceValueFactory
 from tests.factories import ImageFactory, UserFactory, WorkstationFactory
+from tests.hanging_protocols_tests.factories import HangingProtocolFactory
 from tests.reader_studies_tests import RESOURCE_PATH
 from tests.reader_studies_tests.factories import (
     AnswerFactory,
@@ -385,7 +387,10 @@ def test_image_port_only_with_bounding_box(
 
 
 @pytest.mark.django_db
-def test_reader_study_copy(client):
+def test_reader_study_copy(client, settings):
+    settings.task_eager_propagates = (True,)
+    settings.task_always_eager = (True,)
+
     rs = ReaderStudyFactory(title="copied")
     editor = UserFactory()
     editor2 = UserFactory()
@@ -405,9 +410,14 @@ def test_reader_study_copy(client):
     )
 
     im1, im2 = ImageFactory(), ImageFactory()
-
-    rs.images.set([im1, im2])
-    rs.hanging_list = [{"main": im1.name}, {"main": im2.name}]
+    interfaces = []
+    for im in [im1, im2]:
+        civ = ComponentInterfaceValueFactory(image=im)
+        interfaces.append(civ.interface.slug)
+        ds = DisplaySetFactory(reader_study=rs)
+        ds.values.add(civ)
+    rs.view_content = {"main": interfaces[0], "secondary": interfaces[1]}
+    rs.hanging_protocol = HangingProtocolFactory()
     rs.case_text = {im1.name: "test", im2.name: "test2"}
     rs.save()
 
@@ -459,11 +469,12 @@ def test_reader_study_copy(client):
 
     _rs = ReaderStudy.objects.order_by("created").last()
     assert _rs.title == "1"
-    assert _rs.images.count() == 0
+    assert _rs.display_sets.count() == 0
     assert _rs.questions.count() == 0
+    assert _rs.view_content == {}
+    assert _rs.hanging_protocol is None
     assert _rs.readers_group.user_set.count() == 0
     assert _rs.editors_group.user_set.count() == 1
-    assert _rs.hanging_list == []
     assert _rs.case_text == {}
 
     response = get_view_for_user(
@@ -482,21 +493,23 @@ def test_reader_study_copy(client):
     _rs = ReaderStudy.objects.order_by("created").last()
     assert _rs.title == "2"
     assert _rs.questions.count() == 2
-    assert _rs.images.count() == 0
-    assert _rs.hanging_list == []
+    assert _rs.display_sets.count() == 0
+    assert _rs.view_content == {}
+    assert _rs.hanging_protocol is None
     assert _rs.case_text == {}
     assert _rs.readers_group.user_set.count() == 0
     assert _rs.editors_group.user_set.count() == 1
 
-    response = get_view_for_user(
-        viewname="reader-studies:copy",
-        client=client,
-        method=client.post,
-        reverse_kwargs={"slug": rs.slug},
-        data={"title": "3", "copy_images": True},
-        user=editor,
-        follow=True,
-    )
+    with capture_on_commit_callbacks(execute=True):
+        response = get_view_for_user(
+            viewname="reader-studies:copy",
+            client=client,
+            method=client.post,
+            reverse_kwargs={"slug": rs.slug},
+            data={"title": "3", "copy_display_sets": True},
+            user=editor,
+            follow=True,
+        )
 
     assert response.status_code == 200
     assert ReaderStudy.objects.count() == 4
@@ -504,38 +517,28 @@ def test_reader_study_copy(client):
     _rs = ReaderStudy.objects.order_by("created").last()
     assert _rs.title == "3"
     assert _rs.questions.count() == 0
-    assert _rs.images.count() == 2
-    assert _rs.hanging_list == []
+    assert _rs.display_sets.count() == 2
+    assert _rs.view_content == {}
+    assert _rs.hanging_protocol is None
     assert _rs.case_text == {}
     assert _rs.readers_group.user_set.count() == 0
     assert _rs.editors_group.user_set.count() == 1
 
-    response = get_view_for_user(
-        viewname="reader-studies:copy",
-        client=client,
-        method=client.post,
-        reverse_kwargs={"slug": rs.slug},
-        data={"title": "4", "copy_hanging_list": True},
-        user=editor,
-        follow=True,
-    )
-
-    assert response.status_code == 200
-    assert (
-        "Hanging list and case text can only be copied if the images are copied as well"
-        in response.rendered_content
-    )
-    assert ReaderStudy.objects.count() == 4
-
-    response = get_view_for_user(
-        viewname="reader-studies:copy",
-        client=client,
-        method=client.post,
-        reverse_kwargs={"slug": rs.slug},
-        data={"title": "4", "copy_images": True, "copy_hanging_list": True},
-        user=editor,
-        follow=True,
-    )
+    with capture_on_commit_callbacks(execute=True):
+        response = get_view_for_user(
+            viewname="reader-studies:copy",
+            client=client,
+            method=client.post,
+            reverse_kwargs={"slug": rs.slug},
+            data={
+                "title": "4",
+                "copy_display_sets": True,
+                "copy_view_content": True,
+                "copy_hanging_protocol": True,
+            },
+            user=editor,
+            follow=True,
+        )
 
     assert response.status_code == 200
     assert ReaderStudy.objects.count() == 5
@@ -543,21 +546,27 @@ def test_reader_study_copy(client):
     _rs = ReaderStudy.objects.order_by("created").last()
     assert _rs.title == "4"
     assert _rs.questions.count() == 0
-    assert _rs.images.count() == 2
-    assert _rs.hanging_list == rs.hanging_list
+    assert _rs.display_sets.count() == 2
+    assert _rs.view_content == rs.view_content
+    assert _rs.hanging_protocol == rs.hanging_protocol
     assert _rs.case_text == {}
     assert _rs.readers_group.user_set.count() == 0
     assert _rs.editors_group.user_set.count() == 1
 
-    response = get_view_for_user(
-        viewname="reader-studies:copy",
-        client=client,
-        method=client.post,
-        reverse_kwargs={"slug": rs.slug},
-        data={"title": "5", "copy_images": True, "copy_case_text": True},
-        user=editor,
-        follow=True,
-    )
+    with capture_on_commit_callbacks(execute=True):
+        response = get_view_for_user(
+            viewname="reader-studies:copy",
+            client=client,
+            method=client.post,
+            reverse_kwargs={"slug": rs.slug},
+            data={
+                "title": "5",
+                "copy_display_sets": True,
+                "copy_case_text": True,
+            },
+            user=editor,
+            follow=True,
+        )
 
     assert response.status_code == 200
     assert ReaderStudy.objects.count() == 6
@@ -565,8 +574,9 @@ def test_reader_study_copy(client):
     _rs = ReaderStudy.objects.order_by("created").last()
     assert _rs.title == "5"
     assert _rs.questions.count() == 0
-    assert _rs.images.count() == 2
-    assert _rs.hanging_list == []
+    assert _rs.display_sets.count() == 2
+    assert _rs.view_content == {}
+    assert _rs.hanging_protocol is None
     assert _rs.case_text == rs.case_text
     assert _rs.readers_group.user_set.count() == 0
     assert _rs.editors_group.user_set.count() == 1
@@ -587,8 +597,9 @@ def test_reader_study_copy(client):
     _rs = ReaderStudy.objects.order_by("created").last()
     assert _rs.title == "6"
     assert _rs.questions.count() == 0
-    assert _rs.images.count() == 0
-    assert _rs.hanging_list == []
+    assert _rs.display_sets.count() == 0
+    assert _rs.view_content == {}
+    assert _rs.hanging_protocol is None
     assert _rs.case_text == {}
     assert _rs.readers_group.user_set.count() == 1
     assert _rs.editors_group.user_set.count() == 1
@@ -609,8 +620,9 @@ def test_reader_study_copy(client):
     _rs = ReaderStudy.objects.order_by("created").last()
     assert _rs.title == "7"
     assert _rs.questions.count() == 0
-    assert _rs.images.count() == 0
-    assert _rs.hanging_list == []
+    assert _rs.display_sets.count() == 0
+    assert _rs.view_content == {}
+    assert _rs.hanging_protocol is None
     assert _rs.case_text == {}
     assert _rs.readers_group.user_set.count() == 0
     assert _rs.editors_group.user_set.count() == 2