From b4b0cfde3208aacec65320fa9bb3e20139de22f1 Mon Sep 17 00:00:00 2001
From: Parbhat Puri <parbhatpuri17@gmail.com>
Date: Mon, 5 Aug 2019 13:58:44 +0000
Subject: [PATCH] GH-717: Admin changes to allow multiple forms in second stage

---
 opentech/apply/funds/admin_forms.py           | 33 +++++++++++-
 .../0066_add_stage_to_selected_forms.py       | 31 +++++++++++
 ...7_data_migration_for_one_form_per_stage.py | 52 +++++++++++++++++++
 opentech/apply/funds/models/applications.py   |  5 +-
 opentech/apply/funds/models/forms.py          | 10 +++-
 opentech/apply/funds/tests/test_admin_form.py | 29 ++++++++---
 6 files changed, 149 insertions(+), 11 deletions(-)
 create mode 100644 opentech/apply/funds/migrations/0066_add_stage_to_selected_forms.py
 create mode 100644 opentech/apply/funds/migrations/0067_data_migration_for_one_form_per_stage.py

diff --git a/opentech/apply/funds/admin_forms.py b/opentech/apply/funds/admin_forms.py
index f668a26ec..bfcf8ae8e 100644
--- a/opentech/apply/funds/admin_forms.py
+++ b/opentech/apply/funds/admin_forms.py
@@ -1,3 +1,4 @@
+from collections import Counter
 from wagtail.admin.forms import WagtailAdminPageForm
 
 from .workflow import WORKFLOWS
@@ -10,12 +11,42 @@ class WorkflowFormAdminForm(WagtailAdminPageForm):
         workflow = WORKFLOWS[cleaned_data['workflow_name']]
         application_forms = self.formsets['forms']
         review_forms = self.formsets['review_forms']
+        number_of_stages = len(workflow.stages)
 
-        self.validate_stages_equal_forms(workflow, application_forms)
+        self.validate_application_forms(workflow, application_forms)
+        if number_of_stages == 1:
+            self.validate_stages_equal_forms(workflow, application_forms)
         self.validate_stages_equal_forms(workflow, review_forms, form_type="Review form")
 
         return cleaned_data
 
+    def validate_application_forms(self, workflow, forms):
+        """
+        Application forms are not equal to the number of stages like review forms.
+        Now, staff can select a proposal form from multiple forms list in stage 2.
+        """
+        if forms.is_valid():
+            valid_forms = [form for form in forms if not form.cleaned_data['DELETE']]
+            forms_stages = [form.cleaned_data['stage'] for form in valid_forms]
+            stages_counter = Counter(forms_stages)
+
+            number_of_stages = len(workflow.stages)
+            error_list = []
+
+            for stage in range(1, number_of_stages + 1):
+                is_form_present = True if stages_counter.get(stage, 0) > 0 else False
+                if not is_form_present:
+                    error_list.append(f'Please provide form for Stage {stage}.')
+
+                if stage == 1 and stages_counter.get(stage, 0) > 1:
+                    error_list.append('Only 1 form can be selected for 1st Stage.')
+
+            if error_list:
+                self.add_error(
+                    None,
+                    error_list,
+                )
+
     def validate_stages_equal_forms(self, workflow, forms, form_type="form"):
         if forms.is_valid():
             valid_forms = [form for form in forms if not form.cleaned_data['DELETE']]
diff --git a/opentech/apply/funds/migrations/0066_add_stage_to_selected_forms.py b/opentech/apply/funds/migrations/0066_add_stage_to_selected_forms.py
new file mode 100644
index 000000000..4a9e9ae10
--- /dev/null
+++ b/opentech/apply/funds/migrations/0066_add_stage_to_selected_forms.py
@@ -0,0 +1,31 @@
+# Generated by Django 2.0.13 on 2019-08-05 07:43
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('funds', '0065_applicationsubmission_meta_categories'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='applicationbaseform',
+            name='stage',
+            field=models.PositiveSmallIntegerField(choices=[(1, '1st Stage'), (2, '2nd Stage')], default=1),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='labbaseform',
+            name='stage',
+            field=models.PositiveSmallIntegerField(choices=[(1, '1st Stage'), (2, '2nd Stage')], default=1),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='roundbaseform',
+            name='stage',
+            field=models.PositiveSmallIntegerField(choices=[(1, '1st Stage'), (2, '2nd Stage')], default=1),
+            preserve_default=False,
+        ),
+    ]
diff --git a/opentech/apply/funds/migrations/0067_data_migration_for_one_form_per_stage.py b/opentech/apply/funds/migrations/0067_data_migration_for_one_form_per_stage.py
new file mode 100644
index 000000000..4fc31985f
--- /dev/null
+++ b/opentech/apply/funds/migrations/0067_data_migration_for_one_form_per_stage.py
@@ -0,0 +1,52 @@
+# Generated by Django 2.0.13 on 2019-08-05 08:25
+
+from django.db import migrations
+
+
+def increment_stage_in_forms(forms):
+    """
+    The current system has assumption that there is one application form per stage.
+    To replicate the current behaviour new stage field should be equal to the index of the form.
+    """
+    for index, form in enumerate(forms.all(), 1):
+        form.stage = index
+        form.save(update_fields=['stage'])
+
+
+def one_application_form_per_stage(apps, schema_editor):
+    Fund = apps.get_model('funds', 'FundType')
+    RequestForPartners = apps.get_model('funds', 'RequestForPartners')
+    Round = apps.get_model('funds', 'Round')
+    SealedRound = apps.get_model('funds', 'SealedRound')
+    LabType = apps.get_model('funds', 'LabType')
+
+    for fund in Fund.objects.all():
+        if fund.forms.count() > 1:
+            increment_stage_in_forms(fund.forms)
+
+    for rfp in RequestForPartners.objects.all():
+        if rfp.forms.count() > 1:
+            increment_stage_in_forms(rfp.forms)
+
+    for round_ in Round.objects.all():
+        if round_.forms.count() > 1:
+            increment_stage_in_forms(round_.forms)
+
+    for sealed_round in SealedRound.objects.all():
+        if sealed_round.forms.count() > 1:
+            increment_stage_in_forms(sealed_round.forms)
+
+    for lab in LabType.objects.all():
+        if lab.forms.count() > 1:
+            increment_stage_in_forms(lab.forms)
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('funds', '0066_add_stage_to_selected_forms'),
+    ]
+
+    operations = [
+        migrations.RunPython(one_application_form_per_stage, migrations.RunPython.noop),
+    ]
diff --git a/opentech/apply/funds/models/applications.py b/opentech/apply/funds/models/applications.py
index 0fad33145..8443eba7a 100644
--- a/opentech/apply/funds/models/applications.py
+++ b/opentech/apply/funds/models/applications.py
@@ -223,7 +223,10 @@ class RoundBase(WorkflowStreamForm, SubmittableStreamForm):  # type: ignore
         new_form.id = None
         new_form.name = '{} for {} ({})'.format(new_form.name, self.title, self.get_parent().title)
         new_form.save()
-        new_class.objects.create(round=self, form=new_form)
+        if hasattr(form, 'stage'):
+            new_class.objects.create(round=self, form=new_form, stage=form.stage)
+        else:
+            new_class.objects.create(round=self, form=new_form)
 
     def get_submit_meta_data(self, **kwargs):
         return super().get_submit_meta_data(
diff --git a/opentech/apply/funds/models/forms.py b/opentech/apply/funds/models/forms.py
index 40bf70d73..088841286 100644
--- a/opentech/apply/funds/models/forms.py
+++ b/opentech/apply/funds/models/forms.py
@@ -27,10 +27,18 @@ class ApplicationForm(models.Model):
 
 
 class AbstractRelatedForm(Orderable):
+    FIRST_STAGE = 1
+    SECOND_STAGE = 2
+    STAGE_CHOICES = [
+        (FIRST_STAGE, '1st Stage'),
+        (SECOND_STAGE, '2nd Stage'),
+    ]
     form = models.ForeignKey('ApplicationForm', on_delete=models.PROTECT)
+    stage = models.PositiveSmallIntegerField(choices=STAGE_CHOICES)
 
     panels = [
-        FilteredFieldPanel('form', filter_query={'roundbaseform__isnull': True})
+        FilteredFieldPanel('form', filter_query={'roundbaseform__isnull': True}),
+        FieldPanel('stage'),
     ]
 
     @property
diff --git a/opentech/apply/funds/tests/test_admin_form.py b/opentech/apply/funds/tests/test_admin_form.py
index 93413519a..aa0c78f0e 100644
--- a/opentech/apply/funds/tests/test_admin_form.py
+++ b/opentech/apply/funds/tests/test_admin_form.py
@@ -8,7 +8,7 @@ from .factories import ApplicationFormFactory, FundTypeFactory, workflow_for_sta
 from opentech.apply.review.tests.factories import ReviewFormFactory
 
 
-def formset_base(field, total, delete, factory, same=False):
+def formset_base(field, total, delete, factory, same=False, form_stage_info=None):
     base_data = {
         f'{field}-TOTAL_FORMS': total + delete,
         f'{field}-INITIAL_FORMS': 0,
@@ -29,14 +29,18 @@ def formset_base(field, total, delete, factory, same=False):
             f'{field}-{i}-ORDER': i,
             f'{field}-{i}-DELETE': should_delete,
         })
+        if form_stage_info:
+            # form_stage_info contains stage number for selected application forms
+            stage = form_stage_info[i]
+            base_data[f'{field}-{i}-stage'] = stage
         deleted += 1
 
     return base_data
 
 
-def form_data(number_forms=0, delete=0, stages=1, same_forms=False):
-    form_data = formset_base('forms', number_forms, delete, same=same_forms, factory=ApplicationFormFactory)
-    review_form_data = formset_base('review_forms', number_forms, False, same=same_forms, factory=ReviewFormFactory)
+def form_data(number_application_forms=0, number_review_forms=0, delete=0, stages=1, same_forms=False, form_stage_info=[1]):
+    form_data = formset_base('forms', number_application_forms, delete, same=same_forms, factory=ApplicationFormFactory, form_stage_info=form_stage_info)
+    review_form_data = formset_base('review_forms', number_review_forms, False, same=same_forms, factory=ReviewFormFactory)
     form_data.update(review_form_data)
 
     fund_data = factory.build(dict, FACTORY_CLASS=FundTypeFactory)
@@ -58,15 +62,15 @@ class TestWorkflowFormAdminForm(TestCase):
         self.assertTrue(form.errors['__all__'])
 
     def test_validates_with_one_form_one_stage(self):
-        form = self.submit_data(form_data(1))
+        form = self.submit_data(form_data(1, 1))
         self.assertTrue(form.is_valid(), form.errors.as_text())
 
     def test_validates_with_one_form_one_stage_with_deleted(self):
-        form = self.submit_data(form_data(1, delete=1))
+        form = self.submit_data(form_data(1, 1, delete=1, form_stage_info=[2, 1]))
         self.assertTrue(form.is_valid(), form.errors.as_text())
 
     def test_doesnt_validates_with_two_forms_one_stage(self):
-        form = self.submit_data(form_data(2))
+        form = self.submit_data(form_data(2, 2, form_stage_info=[1, 2]))
         self.assertFalse(form.is_valid())
         self.assertTrue(form.errors['__all__'])
         formset_errors = form.formsets['forms'].errors
@@ -76,5 +80,14 @@ class TestWorkflowFormAdminForm(TestCase):
         self.assertTrue(formset_errors[1]['form'])
 
     def test_can_save_two_forms(self):
-        form = self.submit_data(form_data(2, stages=2))
+        form = self.submit_data(form_data(2, 2, stages=2, form_stage_info=[1, 2]))
         self.assertTrue(form.is_valid())
+
+    def test_can_save_multiple_forms_stage_two(self):
+        form = self.submit_data(form_data(3, 2, stages=2, form_stage_info=[1, 2, 2]))
+        self.assertTrue(form.is_valid())
+
+    def test_doesnt_validates_with_two_first_stage_forms_in_two_stage(self):
+        form = self.submit_data(form_data(2, 2, stages=2, form_stage_info=[1, 1]))
+        self.assertFalse(form.is_valid())
+        self.assertTrue(form.errors['__all__'])
-- 
GitLab