Repository: liqd/adhocracy4

Issue:
No Validation or Error when Phases in Dashboard are set in Illogical Order
URL: https://meinberlin-dev.liqd.net/projekte/module/burgerinnenhaushalt-2-phasen/?mode=list
user: Project initiator
expected behaviour: If I have more than one phase in a module, I would expect them to only be able to be set to occur in 1st proposal, 2nd rating, and 3rd voting time slots. If I make a mistake, I hope I am alerted and cannot publish this.
behaviour: Both 3-phase and 2-phase modules can be published with phases that make no sense in their time line. Users can set up and publish modules with voting phases that occur before rating and proposal phases. There is no validation or error.
important screensize: any
device & browser: any
Comment/Question: moved from mB: https://github.com/liqd/a4-meinberlin/issues/4029

Screenshot?
2-Phase module published with reverse phases:
<img width="600" alt="Bildschirmfoto 2021-12-08 um 12 41 47" src="https://user-images.githubusercontent.com/35491681/145202707-b0d39c50-e5a1-476b-9afa-542cc9a85687.png">




Discussion:

Resolving patch:
diff --git a/adhocracy4/phases/forms.py b/adhocracy4/phases/forms.py
--- a/adhocracy4/phases/forms.py
+++ b/adhocracy4/phases/forms.py
@@ -4,9 +4,7 @@
 
 class PhaseInlineFormSet(BaseInlineFormSet):
     def clean(self):
-        """
-        Make sure phases of the same module don't overlap.
-        """
+        """Make sure phases of the same module don't overlap."""
         super().clean()
         phase_dates = []
         for form in self.forms:
@@ -16,6 +14,7 @@ def clean(self):
                     and form.cleaned_data['end_date'] is not None:
                 start_date = form.cleaned_data['start_date']
                 end_date = form.cleaned_data['end_date']
+                weight = form.instance.weight
                 if phase_dates:
                     for phase_date in phase_dates:
                         if (start_date < phase_date[1]
@@ -23,5 +22,12 @@ def clean(self):
                             msg = _('Phases cannot run at the same time '
                                     'and must follow after each other.')
                             form.add_error('end_date', msg)
+                        if ((start_date < phase_date[0]
+                                and weight > phase_date[2])
+                                or (start_date > phase_date[0]
+                                    and weight < phase_date[2])):
+                            msg = _('Phases need to be in same order '
+                                    'as in form.')
+                            form.add_error('start_date', msg)
                 if start_date and end_date:
-                    phase_dates.append((start_date, end_date))
+                    phase_dates.append((start_date, end_date, weight))


Test patch:
diff --git a/tests/dashboard/components/test_views_module_phases.py b/tests/dashboard/components/test_views_module_phases.py
--- a/tests/dashboard/components/test_views_module_phases.py
+++ b/tests/dashboard/components/test_views_module_phases.py
@@ -140,3 +140,48 @@ def test_edit_view_validation_2(client, project, module_factory,
     assert ({'end_date':
             ['End date can not be before start date.']}
             in errors)
+
+
+@pytest.mark.django_db
+def test_edit_view_validation_3(client, project, module_factory,
+                                phase_factory, admin):
+    module = module_factory(project=project)
+    phase_0 = phase_factory(module=module,
+                            weight=1)
+    phase_1 = phase_factory(module=module,
+                            weight=2)
+    url = component.get_base_url(module)
+    client.login(username=admin.username, password='password')
+    response = client.get(url)
+    assert_template_response(response, 'a4dashboard/base_form_module.html')
+
+    data = {
+        'phase_set-TOTAL_FORMS': '2',
+        'phase_set-INITIAL_FORMS': '2',
+        'phase_set-MIN_NUM_FORMS': '0',
+        'phase_set-MAX_NUM_FORMS': '1000',
+
+        'phase_set-0-name': 'Name 0',
+        'phase_set-0-description': 'some description for phase 0',
+        'phase_set-0-start_date_0': '2020-08-23',
+        'phase_set-0-start_date_1': '19:23',
+        'phase_set-0-end_date_0': '2020-09-17',
+        'phase_set-0-end_date_1': '16:12',
+        'phase_set-0-type': phase_0.type,
+        'phase_set-0-id': phase_0.id,
+
+        'phase_set-1-name': 'Name 1',
+        'phase_set-1-description': 'some description for phase 1',
+        'phase_set-1-start_date_0': '2020-07-22',
+        'phase_set-1-start_date_1': '19:23',
+        'phase_set-1-end_date_0': '2020-08-22',
+        'phase_set-1-end_date_1': '16:12',
+        'phase_set-1-type': phase_1.type,
+        'phase_set-1-id': phase_1.id,
+    }
+
+    response = client.post(url, data)
+    errors = response.context_data['form'].errors
+    assert ({'start_date':
+            ['Phases need to be in same order as in form.']}
+            in errors)