Repository: comic/grand-challenge.org

Issue:
Submission unfindable
> I accidentally submitted that algorithm before I uploaded the evaluation container, and now it’s stuck as a “ghost submission”. It doesn’t show up as a submission anywhere, but I also cannot submit it again.

The submission is visible on `/evaluation/submissions/`, but the admin does not have a way of getting to the submission detail page. The title "Submissions and Evaluations" of `/evaluation/<slug>/admin/` is misleading, as it's only the evaluations. Why is admin also in the url?


Discussion:
How is it even possible to submit if there is no method container yet? 
Ah, it's possible! Maybe we should add a check for that to othe submission form
Definitely!

Resolving patch:
diff --git a/app/grandchallenge/evaluation/forms.py b/app/grandchallenge/evaluation/forms.py
--- a/app/grandchallenge/evaluation/forms.py
+++ b/app/grandchallenge/evaluation/forms.py
@@ -416,6 +416,8 @@ def __init__(self, *args, user, phase: Phase, **kwargs):  # noqa: C901
                     },
                 ),
             )
+            if not self._phase.active_image:
+                self.fields["algorithm"].disabled = True
         else:
             del self.fields["algorithm"]
             del self.fields["algorithm_image"]
@@ -426,6 +428,18 @@ def __init__(self, *args, user, phase: Phase, **kwargs):  # noqa: C901
                 "uploads.change_userupload",
             ).filter(status=UserUpload.StatusChoices.COMPLETED)
 
+            if not self._phase.active_image:
+                self.fields["user_upload"].disabled = True
+
+    def clean(self):
+        cleaned_data = super().clean()
+        if not self._phase.active_image:
+            raise ValidationError(
+                "You cannot submit to this phase because this phase "
+                "does not have an active evaluation method yet."
+            )
+        return cleaned_data
+
     def clean_phase(self):
         phase = self.cleaned_data["phase"]
         if (


Test patch:
diff --git a/app/tests/evaluation_tests/test_forms.py b/app/tests/evaluation_tests/test_forms.py
--- a/app/tests/evaluation_tests/test_forms.py
+++ b/app/tests/evaluation_tests/test_forms.py
@@ -247,12 +247,60 @@ def test_no_algorithm_selection(self):
 
         assert form.errors["algorithm"] == ["This field is required."]
 
+    @pytest.mark.parametrize(
+        "submission_kind,disabled_field_name",
+        (
+            [SubmissionKindChoices.CSV, "user_upload"],
+            [SubmissionKindChoices.ALGORITHM, "algorithm"],
+        ),
+    )
+    def test_no_submission_possible_without_method(
+        self, submission_kind, disabled_field_name
+    ):
+        phase = PhaseFactory(submission_kind=submission_kind)
+        form = SubmissionForm(
+            user=UserFactory(),
+            phase=phase,
+            data={"creator": UserFactory(), "phase": phase},
+        )
+        assert form.fields[disabled_field_name].disabled
+        assert not form.is_valid()
+        assert (
+            "You cannot submit to this phase because this phase does "
+            "not have an active evaluation method yet." in str(form.errors)
+        )
+
+        MethodFactory(
+            phase=phase,
+            is_manifest_valid=True,
+            is_in_registry=True,
+            is_desired_version=True,
+        )
+        del phase.active_image
+
+        form = SubmissionForm(
+            user=UserFactory(),
+            phase=phase,
+            data={"creator": UserFactory(), "phase": phase},
+        )
+        assert not form.is_valid()
+        assert not form.fields[disabled_field_name].disabled
+        assert (
+            "You cannot submit to this phase because this phase does "
+            "not have an active evaluation method yet." not in str(form.errors)
+        )
+
     def test_algorithm_no_permission(self):
+        phase = PhaseFactory(submission_kind=SubmissionKindChoices.ALGORITHM)
+        MethodFactory(
+            phase=phase,
+            is_manifest_valid=True,
+            is_in_registry=True,
+            is_desired_version=True,
+        )
         form = SubmissionForm(
             user=UserFactory(),
-            phase=PhaseFactory(
-                submission_kind=SubmissionKindChoices.ALGORITHM
-            ),
+            phase=phase,
             data={"algorithm": AlgorithmFactory()},
         )
 
@@ -297,6 +345,12 @@ def test_algorithm_with_permission(self):
             algorithm=alg,
         )
         AlgorithmJobFactory(algorithm_image=ai, status=4)
+        MethodFactory(
+            is_manifest_valid=True,
+            is_in_registry=True,
+            is_desired_version=True,
+            phase=p,
+        )
 
         form = SubmissionForm(
             user=user,
@@ -346,6 +400,12 @@ def test_algorithm_image_and_model_set(self):
         )
         am = AlgorithmModelFactory(algorithm=alg, is_desired_version=True)
         AlgorithmJobFactory(algorithm_image=ai, status=Job.SUCCESS)
+        MethodFactory(
+            is_manifest_valid=True,
+            is_in_registry=True,
+            is_desired_version=True,
+            phase=p,
+        )
 
         form = SubmissionForm(
             user=user,
@@ -419,6 +479,18 @@ def test_no_valid_archive_items(self):
         )
         p_alg.algorithm_inputs.set([ci1])
         p_alg.algorithm_outputs.set([ci2])
+        MethodFactory(
+            phase=p_alg,
+            is_manifest_valid=True,
+            is_in_registry=True,
+            is_desired_version=True,
+        )
+        MethodFactory(
+            phase=p_pred,
+            is_manifest_valid=True,
+            is_in_registry=True,
+            is_desired_version=True,
+        )
 
         for p in [p_alg, p_pred]:
             InvoiceFactory(
@@ -513,6 +585,12 @@ def test_submission_or_eval_exists_for_image(self):
             phase=p,
             algorithm_image=ai,
         )
+        MethodFactory(
+            phase=p,
+            is_manifest_valid=True,
+            is_in_registry=True,
+            is_desired_version=True,
+        )
 
         form = SubmissionForm(
             user=user,