Repository: comic/grand-challenge.org

Issue:
Deleting images from reader studies
I added a list of images to my reader study on grand-challenge, but it appears that I cannot delete them. Could this functionality be added to grand-challenge? Many thanks in advance. 



Discussion:

Resolving patch:
diff --git a/app/grandchallenge/reader_studies/urls.py b/app/grandchallenge/reader_studies/urls.py
--- a/app/grandchallenge/reader_studies/urls.py
+++ b/app/grandchallenge/reader_studies/urls.py
@@ -9,6 +9,7 @@
     ReaderStudyCreate,
     ReaderStudyDelete,
     ReaderStudyDetail,
+    ReaderStudyImages,
     ReaderStudyLeaderBoard,
     ReaderStudyList,
     ReaderStudyStatistics,
@@ -35,6 +36,7 @@
         ReaderStudyLeaderBoard.as_view(),
         name="leaderboard",
     ),
+    path("<slug>/images/", ReaderStudyImages.as_view(), name="images",),
     path(
         "<slug>/statistics/",
         ReaderStudyStatistics.as_view(),
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
@@ -11,6 +11,7 @@
 )
 from django.contrib.messages.views import SuccessMessageMixin
 from django.core.exceptions import ValidationError
+from django.core.paginator import Paginator
 from django.http import Http404, HttpResponse
 from django.shortcuts import get_object_or_404
 from django.urls import reverse
@@ -43,7 +44,7 @@
 from rest_framework_guardian.filters import ObjectPermissionsFilter
 
 from grandchallenge.cases.forms import UploadRawImagesForm
-from grandchallenge.cases.models import RawImageUploadSession
+from grandchallenge.cases.models import Image, RawImageUploadSession
 from grandchallenge.core.permissions.rest_framework import (
     DjangoObjectOnlyPermissions,
 )
@@ -199,6 +200,25 @@ class ReaderStudyStatistics(
     # If the permission is changed to 'read', we need to filter these values out.
 
 
+class ReaderStudyImages(
+    LoginRequiredMixin, ObjectPermissionRequiredMixin, DetailView
+):
+    model = ReaderStudy
+    permission_required = (
+        f"{ReaderStudy._meta.app_label}.change_{ReaderStudy._meta.model_name}"
+    )
+    raise_exception = True
+    template_name = "reader_studies/readerstudy_images.html"
+
+    def get_context_data(self, **kwarsg):
+        context = super().get_context_data(**kwarsg)
+        paginator = Paginator(self.object.images.all(), 15)
+        page_number = self.request.GET.get("page", 1)
+        page_obj = paginator.get_page(page_number)
+        context.update({"page_obj": page_obj})
+        return context
+
+
 class QuestionUpdate(
     LoginRequiredMixin, ObjectPermissionRequiredMixin, UpdateView
 ):
@@ -459,6 +479,26 @@ def generate_hanging_list(self, request, pk=None):
         )
         return Response({"status": "Hanging list generated."},)
 
+    @action(detail=True, methods=["patch"])
+    def remove_image(self, request, pk=None):
+        image_id = request.data.get("image")
+        reader_study = self.get_object()
+        try:
+            reader_study.images.remove(Image.objects.get(id=image_id))
+            messages.add_message(
+                request, messages.SUCCESS, "Image removed from reader study."
+            )
+            return Response({"status": "Image removed from reader study."},)
+        except Image.DoesNotExist:
+            messages.add_message(
+                request,
+                messages.ERROR,
+                "Image could not be removed from reader study.",
+            )
+        return Response(
+            {"status": "Image could not be removed from reader study."},
+        )
+
 
 class QuestionViewSet(ReadOnlyModelViewSet):
     serializer_class = QuestionSerializer


Test patch:
diff --git a/app/tests/reader_studies_tests/test_api.py b/app/tests/reader_studies_tests/test_api.py
--- a/app/tests/reader_studies_tests/test_api.py
+++ b/app/tests/reader_studies_tests/test_api.py
@@ -503,3 +503,60 @@ def test_generate_hanging_list_api_view(generate_hanging_list, client):
     assert response.status_code == 200
     assert "Hanging list generated." in str(response.content)
     generate_hanging_list.assert_called_once()
+
+
+@pytest.mark.django_db
+def test_remove_image_api_view(client):
+    rs = ReaderStudyFactory()
+    reader, editor = UserFactory(), UserFactory()
+    rs.add_reader(reader)
+    rs.add_editor(editor)
+
+    response = get_view_for_user(
+        viewname="api:reader-study-remove-image",
+        reverse_kwargs={"pk": rs.pk},
+        user=reader,
+        client=client,
+        method=client.patch,
+        data={"image": 1},
+        content_type="application/json",
+        follow=True,
+    )
+
+    assert response.status_code == 403
+
+    response = get_view_for_user(
+        viewname="api:reader-study-remove-image",
+        reverse_kwargs={"pk": rs.pk},
+        user=editor,
+        client=client,
+        method=client.patch,
+        data={"image": 1},
+        content_type="application/json",
+        follow=True,
+    )
+
+    assert response.status_code == 200
+    assert "Image could not be removed from reader study." in str(
+        response.content
+    )
+
+    im = ImageFactory()
+    rs.images.add(im)
+
+    assert im in rs.images.all()
+
+    response = get_view_for_user(
+        viewname="api:reader-study-remove-image",
+        reverse_kwargs={"pk": rs.pk},
+        user=editor,
+        client=client,
+        method=client.patch,
+        data={"image": im.pk},
+        content_type="application/json",
+        follow=True,
+    )
+
+    assert response.status_code == 200
+    assert "Image removed from reader study." in str(response.content)
+    assert im not in rs.images.all()