Repository: comic/grand-challenge.org

Issue:
Email the algorithm user and algorithm editors when an algorithm job fails
At the moment there is not any detailed information about why an algorithm job failed available to the algortihm user. Add emails for this to the job creator, and all editors of that algorithm. Include the last line of the traceback (see the evaluation job failure emails for inspiration).


Discussion:

Resolving patch:
diff --git a/app/grandchallenge/algorithms/emails.py b/app/grandchallenge/algorithms/emails.py
--- a/app/grandchallenge/algorithms/emails.py
+++ b/app/grandchallenge/algorithms/emails.py
@@ -1,6 +1,9 @@
+from django.conf import settings
 from django.contrib.sites.models import Site
+from django.core.mail import send_mail
 
 from grandchallenge.core.utils.email import send_templated_email
+from grandchallenge.evaluation.templatetags.evaluation_extras import user_error
 
 
 def send_permission_request_email(obj):
@@ -77,3 +80,33 @@ def send_permission_denied_email(obj):
         kwargs,
         [obj.user.email],
     )
+
+
+def send_failed_job_email(job):
+    algorithm = job.algorithm_image.algorithm
+    message = (
+        f"Unfortunately your job for algorithm "
+        f"'{algorithm.title}' failed with an error. "
+        f"The error message is:\n\n"
+        f"{user_error(job.output)}\n\n"
+        f"You may wish to try and correct this, or contact the challenge "
+        f"organizers. The following information may help them:\n"
+        f"User: {job.creator.username}\n"
+        f"Job ID: {job.pk}\n"
+        f"Submission ID: {job.pk}"
+    )
+    recipient_emails = [
+        o.email for o in algorithm.editors_group.user_set.all()
+    ]
+    recipient_emails.append(job.creator.email)
+    for email in recipient_emails:
+        send_mail(
+            subject=(
+                f"[{Site.objects.get_current().domain.lower()}] "
+                f"[{algorithm.title.lower()}] "
+                f"Job Failed"
+            ),
+            message=message,
+            from_email=settings.DEFAULT_FROM_EMAIL,
+            recipient_list=[email],
+        )
diff --git a/app/grandchallenge/algorithms/models.py b/app/grandchallenge/algorithms/models.py
--- a/app/grandchallenge/algorithms/models.py
+++ b/app/grandchallenge/algorithms/models.py
@@ -15,6 +15,7 @@
 from django_extensions.db.models import TitleSlugDescriptionModel
 from guardian.shortcuts import assign_perm, get_objects_for_group, remove_perm
 
+from grandchallenge.algorithms.emails import send_failed_job_email
 from grandchallenge.cases.models import RawImageFile, RawImageUploadSession
 from grandchallenge.challenges.models import get_logo_path
 from grandchallenge.container_exec.backends.docker import (
@@ -420,6 +421,14 @@ def assign_permissions(self):
                 f"view_{self.image._meta.model_name}", self.creator, self.image
             )
 
+    def update_status(self, *args, **kwargs):
+        res = super().update_status(*args, **kwargs)
+
+        if self.status == self.FAILURE:
+            send_failed_job_email(self)
+
+        return res
+
 
 class AlgorithmPermissionRequest(RequestBase):
     """


Test patch:
diff --git a/app/tests/algorithms_tests/test_models.py b/app/tests/algorithms_tests/test_models.py
--- a/app/tests/algorithms_tests/test_models.py
+++ b/app/tests/algorithms_tests/test_models.py
@@ -1,8 +1,15 @@
 import pytest
+from django.contrib.sites.models import Site
+from django.core import mail
 from django.core.exceptions import ObjectDoesNotExist
 
-from grandchallenge.algorithms.models import Algorithm
-from tests.algorithms_tests.factories import AlgorithmFactory
+from grandchallenge.algorithms.models import Algorithm, Job
+from tests.algorithms_tests.factories import (
+    AlgorithmFactory,
+    AlgorithmImageFactory,
+    AlgorithmJobFactory,
+)
+from tests.factories import UserFactory
 
 
 @pytest.mark.django_db
@@ -43,3 +50,34 @@ def test_group_deletion_reverse(group):
 
     with pytest.raises(ObjectDoesNotExist):
         algorithm.refresh_from_db()
+
+
+@pytest.mark.django_db
+def test_algorithm_job_update_status():
+    alg = AlgorithmFactory()
+    user = UserFactory()
+    editor = UserFactory()
+
+    alg.add_user(user)
+    alg.add_editor(editor)
+
+    ai = AlgorithmImageFactory(algorithm=alg)
+    job = AlgorithmJobFactory(algorithm_image=ai, creator=user)
+
+    for status, _ in Job.STATUS_CHOICES:
+        job.update_status(status=status)
+        job.refresh_from_db()
+        assert job.status == status
+
+    remaining_recipients = {user.email, editor.email}
+    for email in mail.outbox:
+        remaining_recipients -= set(email.to)
+        assert (
+            email.subject
+            == f"[{Site.objects.get_current().domain.lower()}] [{alg.title.lower()}] Job Failed"
+        )
+        assert (
+            f"Unfortunately your job for algorithm '{alg.title}' failed with an error"
+            in email.body
+        )
+    assert remaining_recipients == set()