diff --git a/opentech/apply/determinations/forms.py b/opentech/apply/determinations/forms.py index b6d2559b5a..f8ae281c94 100644 --- a/opentech/apply/determinations/forms.py +++ b/opentech/apply/determinations/forms.py @@ -344,7 +344,26 @@ class BaseProposalDeterminationForm(forms.Form): class ConceptDeterminationForm(BaseConceptDeterminationForm, BaseNormalDeterminationForm): - pass + def __init__(self, *args, submission, user, initial={}, instance=None, **kwargs): + super().__init__(*args, submission=submission, user=user, initial={}, instance=None, **kwargs) + + action = kwargs.get('action') + stages_num = len(submission.workflow.stages) + + if stages_num > 1 and action == 'invited_to_proposal': + second_stage_forms = submission.get_from_parent('forms').filter(stage=2) + if second_stage_forms.count() > 1: + proposal_form_choices = [ + (index, form.form.name) + for index, form in enumerate(second_stage_forms) + ] + self.fields['proposal_form'] = forms.ChoiceField( + label='Proposal Form', + choices=proposal_form_choices, + help_text='Select the proposal form to use for proposal stage.', + ) + self.fields['proposal_form'].group = 1 + self.fields.move_to_end('proposal_form', last=False) class ProposalDeterminationForm(BaseProposalDeterminationForm, BaseNormalDeterminationForm): diff --git a/opentech/apply/determinations/tests/test_views.py b/opentech/apply/determinations/tests/test_views.py index f3e71f9993..ffb9486fb4 100644 --- a/opentech/apply/determinations/tests/test_views.py +++ b/opentech/apply/determinations/tests/test_views.py @@ -71,7 +71,6 @@ def test_cant_resubmit_determination(self): def test_can_edit_draft_determination(self): submission = ApplicationSubmissionFactory(status='post_review_discussion', lead=self.user) - DeterminationFactory(submission=submission, author=self.user) response = self.post_page(submission, { 'data': 'value', 'outcome': ACCEPTED, diff --git a/opentech/apply/determinations/views.py b/opentech/apply/determinations/views.py index 5e341b491d..b6a0549e1a 100644 --- a/opentech/apply/determinations/views.py +++ b/opentech/apply/determinations/views.py @@ -248,6 +248,7 @@ def form_valid(self, form): submission=self.object.submission, related=self.object, ) + proposal_form = form.cleaned_data.get('proposal_form') transition = transition_from_outcome(form.cleaned_data.get('outcome'), self.submission) if self.object.outcome == NEEDS_MORE_INFO: @@ -260,7 +261,8 @@ def form_valid(self, form): related_object=self.object, ) - self.submission.perform_transition(transition, self.request.user, request=self.request, notify=False) + self.submission.perform_transition( + transition, self.request.user, request=self.request, notify=False, proposal_form=proposal_form) return HttpResponseRedirect(self.submission.get_absolute_url()) 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/models/submissions.py b/opentech/apply/funds/models/submissions.py index 3591d72b2a..a932968d7d 100644 --- a/opentech/apply/funds/models/submissions.py +++ b/opentech/apply/funds/models/submissions.py @@ -310,15 +310,15 @@ def perform_transition(self, action, user, request=None, **kwargs): transition(by=user, request=request, **kwargs) self.save(update_fields=['status']) - self.progress_stage_when_possible(user, request) + self.progress_stage_when_possible(user, request, **kwargs) attrs['perform_transition'] = perform_transition - def progress_stage_when_possible(self, user, request): + def progress_stage_when_possible(self, user, request, notify=None, **kwargs): # Check to see if we can progress to a new stage from the current status for stage_transition in STAGE_CHANGE_ACTIONS: try: - self.perform_transition(stage_transition, user, request=request, notify=False) + self.perform_transition(stage_transition, user, request=request, notify=False, **kwargs) except PermissionDenied: pass @@ -513,7 +513,9 @@ def progress_application(self, **kwargs): prev_meta_categories = submission_in_db.meta_categories.all() self.id = None - self.form_fields = self.get_from_parent('get_defined_fields')(target) + proposal_form = kwargs.get('proposal_form') + proposal_form = int(proposal_form) if proposal_form else 0 + self.form_fields = self.get_from_parent('get_defined_fields')(target, form_index=proposal_form) self.live_revision = None self.draft_revision = None diff --git a/opentech/apply/funds/models/utils.py b/opentech/apply/funds/models/utils.py index f6661614fc..c5131399e0 100644 --- a/opentech/apply/funds/models/utils.py +++ b/opentech/apply/funds/models/utils.py @@ -83,12 +83,12 @@ class WorkflowStreamForm(WorkflowHelpers, AbstractStreamForm): # type: ignore class Meta: abstract = True - def get_defined_fields(self, stage=None): + def get_defined_fields(self, stage=None, form_index=0): if not stage: - form_index = 0 + stage_num = 1 else: - form_index = self.workflow.stages.index(stage) - return self.forms.all()[form_index].fields + stage_num = self.workflow.stages.index(stage) + 1 + return self.forms.filter(stage=stage_num)[form_index].fields def render_landing_page(self, request, form_submission=None, *args, **kwargs): # We only reach this page after creation of a new submission diff --git a/opentech/apply/funds/tests/factories/models.py b/opentech/apply/funds/tests/factories/models.py index d31bd9bf20..6b348db8f0 100644 --- a/opentech/apply/funds/tests/factories/models.py +++ b/opentech/apply/funds/tests/factories/models.py @@ -90,10 +90,11 @@ def parent(self, create, extracted_parent, **parent_kwargs): @factory.post_generation def forms(self, create, extracted, **kwargs): if create: - for _ in self.workflow.stages: + for index, _ in enumerate(self.workflow.stages, 1): # Generate a form based on all defined fields on the model ApplicationBaseFormFactory( application=self, + stage=index, **kwargs, ) ApplicationBaseReviewForm( @@ -160,10 +161,11 @@ def parent(self, create, extracted_parent, **parent_kwargs): @factory.post_generation def forms(self, create, extracted, **kwargs): if create: - for _ in self.workflow.stages: + for index, _ in enumerate(self.workflow.stages, 1): # Generate a form based on all defined fields on the model RoundBaseFormFactory( round=self, + stage=index, **kwargs, ) RoundBaseReviewFormFactory( @@ -197,10 +199,11 @@ class Meta: @factory.post_generation def forms(self, create, extracted, **kwargs): if create: - for _ in self.workflow.stages: + for index, _ in enumerate(self.workflow.stages, 1): # Generate a form based on all defined fields on the model LabBaseFormFactory( lab=self, + stage=index, **kwargs, ) LabBaseReviewFormFactory( diff --git a/opentech/apply/funds/tests/test_admin_form.py b/opentech/apply/funds/tests/test_admin_form.py index 93413519a4..244ece50b1 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,20 @@ 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(num_appl_forms=0, num_review_forms=0, delete=0, stages=1, same_forms=False, form_stage_info=[1]): + form_data = formset_base( + 'forms', num_appl_forms, delete, same=same_forms, factory=ApplicationFormFactory, + form_stage_info=form_stage_info) + review_form_data = formset_base('review_forms', num_review_forms, False, same=same_forms, factory=ReviewFormFactory) form_data.update(review_form_data) fund_data = factory.build(dict, FACTORY_CLASS=FundTypeFactory) @@ -58,15 +64,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 +82,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__']) diff --git a/opentech/apply/funds/tests/test_admin_views.py b/opentech/apply/funds/tests/test_admin_views.py index 9bfc4cc438..4cda56b836 100644 --- a/opentech/apply/funds/tests/test_admin_views.py +++ b/opentech/apply/funds/tests/test_admin_views.py @@ -17,11 +17,12 @@ def setUpTestData(cls): cls.user = SuperUserFactory() cls.home = ApplyHomePageFactory() - def create_page(self, forms=1, same_forms=False): + def create_page(self, appl_forms=1, review_forms=1, stages=1, same_forms=False, form_stage_info=[1]): self.client.force_login(self.user) url = reverse('wagtailadmin_pages:add', args=('funds', 'fundtype', self.home.id)) - data = form_data(forms, same_forms=same_forms, stages=forms) + data = form_data( + appl_forms, review_forms, same_forms=same_forms, stages=stages, form_stage_info=form_stage_info) data['action-publish'] = True response = self.client.post(url, data=data, secure=True, follow=True) @@ -38,12 +39,17 @@ def test_can_create_fund(self): self.assertEqual(fund.review_forms.count(), 1) def test_can_create_multi_phase_fund(self): - fund = self.create_page(2) + fund = self.create_page(2, 2, stages=2, form_stage_info=[1, 2]) self.assertEqual(fund.forms.count(), 2) self.assertEqual(fund.review_forms.count(), 2) + def test_can_create_multiple_forms_second_stage_in_fund(self): + fund = self.create_page(4, 2, stages=2, form_stage_info=[1, 2, 2, 2]) + self.assertEqual(fund.forms.count(), 4) + self.assertEqual(fund.review_forms.count(), 2) + def test_can_create_multi_phase_fund_reuse_forms(self): - fund = self.create_page(2, same_forms=True) + fund = self.create_page(2, 2, same_forms=True, stages=2, form_stage_info=[1, 2]) self.assertEqual(fund.forms.count(), 2) self.assertEqual(fund.review_forms.count(), 2)