Repository: comic/grand-challenge.org

Issue:
sending unsupported file type to algorithm fails silently
some error feedback to the user would be nice.


Discussion:
@MikeOverkamp-diag - we should add emails from the upload sessions, I'd like it if we could fix this after #962 

Resolving patch:
diff --git a/app/grandchallenge/cases/forms.py b/app/grandchallenge/cases/forms.py
--- a/app/grandchallenge/cases/forms.py
+++ b/app/grandchallenge/cases/forms.py
@@ -17,7 +17,11 @@ class UploadRawImagesForm(forms.ModelForm):
     files = UploadedAjaxFileList(
         widget=uploader.AjaxUploadWidget(multifile=True, auto_commit=False),
         label="Image files",
-        help_text=("Upload images for creating a new archive"),
+        help_text=(
+            "Upload images for creating a new archive<br/><br/>"
+            "The following file formats are supported: "
+            ".mha, .mhd, .raw, .zraw, .tiff"
+        ),
     )
 
     def __init__(self, *args, user, **kwargs):
diff --git a/app/grandchallenge/cases/models.py b/app/grandchallenge/cases/models.py
--- a/app/grandchallenge/cases/models.py
+++ b/app/grandchallenge/cases/models.py
@@ -93,7 +93,8 @@ class RawImageUploadSession(UUIDModel):
     def __str__(self):
         return (
             f"Upload Session <{str(self.pk).split('-')[0]}>, "
-            f"({self.session_state})"
+            f"({self.session_state}) "
+            f"{self.error_message or ''}"
         )
 
     def save(self, *args, skip_processing=False, **kwargs):
diff --git a/app/grandchallenge/cases/tasks.py b/app/grandchallenge/cases/tasks.py
--- a/app/grandchallenge/cases/tasks.py
+++ b/app/grandchallenge/cases/tasks.py
@@ -5,6 +5,9 @@
 from uuid import UUID
 
 from celery import shared_task
+from django.conf import settings
+from django.contrib.sites.models import Site
+from django.core.mail import send_mail
 from django.db import transaction
 
 from grandchallenge.algorithms.models import Job
@@ -242,7 +245,6 @@ def build_images(upload_session_uuid: UUID):
                     algorithm_result = algorithm(
                         tmp_dir
                     )  # type: ImageBuilderResult
-
                     collected_images += list(algorithm_result.new_images)
                     collected_associated_files += list(
                         algorithm_result.new_image_files
@@ -260,7 +262,6 @@ def build_images(upload_session_uuid: UUID):
                         msg,
                     ) in algorithm_result.file_errors_map.items():
                         if filename in unconsumed_filenames:
-                            unconsumed_filenames.remove(filename)
                             raw_image = filename_lookup[
                                 filename
                             ]  # type: RawImageFile
@@ -281,6 +282,28 @@ def build_images(upload_session_uuid: UUID):
                         "File could not be processed by any image builder"
                     )
 
+                if unconsumed_filenames:
+                    upload_session.error_message = f"Failed: {', '.join(unconsumed_filenames)}"[
+                        : RawImageUploadSession.max_length_error_message
+                    ]
+                    if upload_session.creator and upload_session.creator.email:
+                        msg = (
+                            "The following image files could not be processed "
+                            f"in reader study {upload_session.reader_study}:"
+                            f"\n\n{', '.join(unconsumed_filenames)}\n\n"
+                            "The following file formats are supported: "
+                            ".mhd, .mha, .tiff"
+                        )
+                        send_mail(
+                            subject=(
+                                f"[{Site.objects.get_current().domain.lower()}] "
+                                f"Unable to process images"
+                            ),
+                            message=msg,
+                            from_email=settings.DEFAULT_FROM_EMAIL,
+                            recipient_list=[upload_session.creator.email],
+                        )
+
                 if upload_session.imageset:
                     upload_session.imageset.images.add(*collected_images)
 


Test patch:
diff --git a/app/tests/cases_tests/test_background_tasks.py b/app/tests/cases_tests/test_background_tasks.py
--- a/app/tests/cases_tests/test_background_tasks.py
+++ b/app/tests/cases_tests/test_background_tasks.py
@@ -10,6 +10,7 @@
 )
 from grandchallenge.jqfileupload.widgets.uploader import StagedAjaxFile
 from tests.cases_tests import RESOURCE_PATH
+from tests.factories import UserFactory
 from tests.jqfileupload_tests.external_test_support import (
     create_file_from_filepath,
 )
@@ -18,8 +19,9 @@
 def create_raw_upload_image_session(
     images: List[str], delete_file=False, imageset=None, annotationset=None
 ) -> Tuple[RawImageUploadSession, Dict[str, RawImageFile]]:
+    creator = UserFactory(email="test@example.com")
     upload_session = RawImageUploadSession(
-        imageset=imageset, annotationset=annotationset
+        imageset=imageset, annotationset=annotationset, creator=creator
     )
 
     uploaded_images = {}
@@ -73,11 +75,14 @@ def test_image_file_creation(settings):
         "valid_tiff.tif",
         "invalid_tiles_tiff.tif",
     ]
+
+    invalid_images = ("no_image", "invalid_utf8.mhd", "invalid_tiles_tiff.tif")
     session, uploaded_images = create_raw_upload_image_session(images)
 
     session.refresh_from_db()
     assert session.session_state == UploadSessionState.stopped
-    assert session.error_message is None
+    for image_name in invalid_images:
+        assert image_name in session.error_message
 
     assert Image.objects.filter(origin=session).count() == 5
 
@@ -88,7 +93,7 @@ def test_image_file_creation(settings):
         db_object.refresh_from_db()
 
         assert db_object.staged_file_id is None
-        if name in ("no_image", "invalid_utf8.mhd", "invalid_tiles_tiff.tif"):
+        if name in invalid_images:
             assert db_object.error is not None
         else:
             assert db_object.error is None
@@ -157,7 +162,8 @@ def test_no_convertible_file(settings):
 
     session.refresh_from_db()
     assert session.session_state == UploadSessionState.stopped
-    assert session.error_message is None
+    for image_name in images:
+        assert image_name in session.error_message
 
     no_image_image = list(uploaded_images.values())[0]
     no_image_image.refresh_from_db()