diff --git a/opentech/apply/funds/admin_forms.py b/opentech/apply/funds/admin_forms.py index f668a26eca..bfcf8ae8e2 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 @@ def clean(self): 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 0000000000..4a9e9ae107 --- /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 0000000000..4fc31985f1 --- /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 0fad33145e..8443eba7a4 100644 --- a/opentech/apply/funds/models/applications.py +++ b/opentech/apply/funds/models/applications.py @@ -223,7 +223,10 @@ def _copy_form(self, form, new_class): 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 40bf70d730..088841286c 100644 --- a/opentech/apply/funds/models/forms.py +++ b/opentech/apply/funds/models/forms.py @@ -27,10 +27,18 @@ def __str__(self): 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 93413519a4..aa0c78f0ef 100644 --- a/opentech/apply/funds/tests/test_admin_form.py +++ b/opentech/apply/funds/tests/test_admin_form.py @@ -8,7 +8,7 @@ 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 @@ def test_doesnt_validates_with_no_form(self): 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 @@ def test_doesnt_validates_with_two_forms_one_stage(self): 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__'])