Repository: comic/grand-challenge.org

Issue:
Add form for challenge admins to add new phases




Discussion:

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
@@ -23,6 +23,8 @@
 from grandchallenge.jqfileupload.widgets.uploader import UploadedAjaxFileList
 from grandchallenge.subdomains.utils import reverse_lazy
 
+phase_options = ("title",)
+
 submission_options = (
     "submission_page_html",
     "daily_submission_limit",
@@ -58,12 +60,35 @@
 )
 
 
-class PhaseForm(forms.ModelForm):
+class PhaseTitleMixin:
+    def __init__(self, *args, challenge, **kwargs):
+        self.challenge = challenge
+        super().__init__(*args, **kwargs)
+
+    def clean_title(self):
+        title = self.cleaned_data["title"]
+
+        if self.challenge.phase_set.filter(title=title).exists():
+            raise ValidationError(
+                "This challenge already has a phase with this title"
+            )
+
+        return title
+
+
+class PhaseCreateForm(PhaseTitleMixin, SaveFormInitMixin, forms.ModelForm):
+    class Meta:
+        model = Phase
+        fields = ("title",)
+
+
+class PhaseUpdateForm(PhaseTitleMixin, forms.ModelForm):
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         self.helper = FormHelper(self)
         self.helper.layout = Layout(
             TabHolder(
+                Tab("Phase", *phase_options),
                 Tab("Submission", *submission_options),
                 Tab("Scoring", *scoring_options),
                 Tab("Leaderboard", *leaderboard_options),
@@ -75,6 +100,7 @@ def __init__(self, *args, **kwargs):
     class Meta:
         model = Phase
         fields = (
+            *phase_options,
             *submission_options,
             *scoring_options,
             *leaderboard_options,
diff --git a/app/grandchallenge/evaluation/urls.py b/app/grandchallenge/evaluation/urls.py
--- a/app/grandchallenge/evaluation/urls.py
+++ b/app/grandchallenge/evaluation/urls.py
@@ -11,6 +11,7 @@
     MethodDetail,
     MethodList,
     ObservableDetail,
+    PhaseCreate,
     PhaseUpdate,
     SubmissionCreate,
     SubmissionDetail,
@@ -24,6 +25,7 @@
     path("<uuid:pk>/", EvaluationDetail.as_view(), name="detail"),
     # UUID should be matched before slugs
     path("<uuid:pk>/update/", EvaluationUpdate.as_view(), name="update"),
+    path("phase/create/", PhaseCreate.as_view(), name="phase-create"),
     path(
         "<slug>/leaderboard/", LeaderboardDetail.as_view(), name="leaderboard"
     ),
diff --git a/app/grandchallenge/evaluation/views/__init__.py b/app/grandchallenge/evaluation/views/__init__.py
--- a/app/grandchallenge/evaluation/views/__init__.py
+++ b/app/grandchallenge/evaluation/views/__init__.py
@@ -31,7 +31,8 @@
 from grandchallenge.evaluation.forms import (
     LegacySubmissionForm,
     MethodForm,
-    PhaseForm,
+    PhaseCreateForm,
+    PhaseUpdateForm,
     SubmissionForm,
 )
 from grandchallenge.evaluation.models import (
@@ -46,13 +47,39 @@
 from grandchallenge.teams.models import Team
 
 
+class PhaseCreate(
+    LoginRequiredMixin,
+    ObjectPermissionRequiredMixin,
+    SuccessMessageMixin,
+    CreateView,
+):
+    model = Phase
+    form_class = PhaseCreateForm
+    success_message = "Phase successfully created"
+    permission_required = "change_challenge"
+    raise_exception = True
+    login_url = reverse_lazy("userena_signin")
+
+    def get_permission_object(self):
+        return self.request.challenge
+
+    def get_form_kwargs(self):
+        kwargs = super().get_form_kwargs()
+        kwargs.update({"challenge": self.request.challenge})
+        return kwargs
+
+    def form_valid(self, form):
+        form.instance.challenge = self.request.challenge
+        return super().form_valid(form)
+
+
 class PhaseUpdate(
     LoginRequiredMixin,
     ObjectPermissionRequiredMixin,
     SuccessMessageMixin,
     UpdateView,
 ):
-    form_class = PhaseForm
+    form_class = PhaseUpdateForm
     success_message = "Configuration successfully updated"
     permission_required = "change_phase"
     raise_exception = True
@@ -63,6 +90,11 @@ def get_object(self, queryset=None):
             challenge=self.request.challenge, slug=self.kwargs["slug"]
         )
 
+    def get_form_kwargs(self):
+        kwargs = super().get_form_kwargs()
+        kwargs.update({"challenge": self.request.challenge})
+        return kwargs
+
     def get_success_url(self):
         return reverse(
             "evaluation:leaderboard",


Test patch:
diff --git a/app/tests/evaluation_tests/test_views.py b/app/tests/evaluation_tests/test_views.py
--- a/app/tests/evaluation_tests/test_views.py
+++ b/app/tests/evaluation_tests/test_views.py
@@ -23,6 +23,7 @@ def test_login_redirect(self, client):
         e = EvaluationFactory()
 
         for view_name, kwargs in [
+            ("phase-create", {}),
             ("phase-update", {"slug": e.submission.phase.slug}),
             ("method-create", {}),
             ("method-list", {}),
@@ -77,6 +78,12 @@ def test_permission_required_views(self, client):
         u = UserFactory()
 
         for view_name, kwargs, permission, obj in [
+            (
+                "phase-create",
+                {},
+                "change_challenge",
+                e.submission.phase.challenge,
+            ),
             (
                 "phase-update",
                 {"slug": e.submission.phase.slug},