From 6c2a3834f6060798f063ea7240c0aded4f82a12f Mon Sep 17 00:00:00 2001
From: sandeepsajan0 <sandeepsajan0@gmail.com>
Date: Wed, 6 Jul 2022 18:17:03 +0530
Subject: [PATCH] Re add the stream field form for PAF

---
 .../migrations/0099_auto_20220706_1234.py     | 25 ++++++++++++++++
 hypha/apply/funds/models/applications.py      | 18 ++++++++++++
 hypha/apply/funds/tests/factories/models.py   |  1 +
 hypha/apply/funds/tests/test_admin_form.py    |  1 +
 hypha/apply/projects/admin.py                 | 17 ++++++++++-
 hypha/apply/projects/forms/project.py         |  5 ++++
 .../migrations/0052_projectapprovalform.py    | 26 +++++++++++++++++
 hypha/apply/projects/models/__init__.py       |  2 ++
 hypha/apply/projects/models/project.py        | 14 +++++++++
 hypha/apply/projects/tests/factories.py       | 20 +++++++++++++
 hypha/apply/projects/views/project.py         | 29 +++++++++++++++++++
 11 files changed, 157 insertions(+), 1 deletion(-)
 create mode 100644 hypha/apply/funds/migrations/0099_auto_20220706_1234.py
 create mode 100644 hypha/apply/projects/migrations/0052_projectapprovalform.py

diff --git a/hypha/apply/funds/migrations/0099_auto_20220706_1234.py b/hypha/apply/funds/migrations/0099_auto_20220706_1234.py
new file mode 100644
index 000000000..7f03ba550
--- /dev/null
+++ b/hypha/apply/funds/migrations/0099_auto_20220706_1234.py
@@ -0,0 +1,25 @@
+# Generated by Django 3.2.13 on 2022-07-06 12:34
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('application_projects', '0052_projectapprovalform'),
+        ('funds', '0098_alter_applicationsubmission_submit_time'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='applicationbase',
+            name='approval_form',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='funds', to='application_projects.projectapprovalform'),
+        ),
+        migrations.AddField(
+            model_name='labbase',
+            name='approval_form',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='labs', to='application_projects.projectapprovalform'),
+        ),
+    ]
diff --git a/hypha/apply/funds/models/applications.py b/hypha/apply/funds/models/applications.py
index ef24ed559..13837638b 100644
--- a/hypha/apply/funds/models/applications.py
+++ b/hypha/apply/funds/models/applications.py
@@ -71,6 +71,14 @@ class ApplicationBase(EmailForm, WorkflowStreamForm):  # type: ignore
         blank=True,
     )
 
+    approval_form = models.ForeignKey(
+        'application_projects.ProjectApprovalForm',
+        blank=True,
+        null=True,
+        on_delete=models.SET_NULL,
+        related_name='funds',
+    )
+
     guide_link = models.URLField(blank=True, max_length=255, help_text=_('Link to the apply guide.'))
 
     slack_channel = models.CharField(blank=True, max_length=128, help_text=_('The slack #channel for notifications. If left empty, notifications will go to the default channel.'))
@@ -108,6 +116,7 @@ class ApplicationBase(EmailForm, WorkflowStreamForm):  # type: ignore
         return self.open_round.serve(request)
 
     content_panels = WorkflowStreamForm.content_panels + [
+        FieldPanel('approval_form'),
         FieldPanel('reviewers', widget=forms.SelectMultiple(attrs={'size': '16'})),
         FieldPanel('guide_link'),
         FieldPanel('slack_channel'),
@@ -409,6 +418,14 @@ class LabBase(EmailForm, WorkflowStreamForm, SubmittableStreamForm):  # type: ig
         blank=True,
     )
 
+    approval_form = models.ForeignKey(
+        'application_projects.ProjectApprovalForm',
+        blank=True,
+        null=True,
+        on_delete=models.SET_NULL,
+        related_name='labs',
+    )
+
     guide_link = models.URLField(blank=True, max_length=255, help_text=_('Link to the apply guide.'))
 
     slack_channel = models.CharField(blank=True, max_length=128, help_text=_('The slack #channel for notifications.'))
@@ -417,6 +434,7 @@ class LabBase(EmailForm, WorkflowStreamForm, SubmittableStreamForm):  # type: ig
     subpage_types = []  # type: ignore
 
     content_panels = WorkflowStreamForm.content_panels + [
+        FieldPanel('approval_form'),
         FieldPanel('lead'),
         FieldPanel('reviewers', widget=forms.SelectMultiple(attrs={'size': '16'})),
         FieldPanel('guide_link'),
diff --git a/hypha/apply/funds/tests/factories/models.py b/hypha/apply/funds/tests/factories/models.py
index 9da35a9b8..59727aa61 100644
--- a/hypha/apply/funds/tests/factories/models.py
+++ b/hypha/apply/funds/tests/factories/models.py
@@ -82,6 +82,7 @@ class AbstractApplicationFactory(wagtail_factories.PageFactory):
 
     # Will need to update how the stages are identified as Fund Page changes
     workflow_name = factory.LazyAttribute(lambda o: workflow_for_stages(o.workflow_stages))
+    approval_form = factory.SubFactory('hypha.apply.projects.tests.factories.ProjectApprovalFormFactory')
 
     @factory.post_generation
     def forms(self, create, extracted, **kwargs):
diff --git a/hypha/apply/funds/tests/test_admin_form.py b/hypha/apply/funds/tests/test_admin_form.py
index 801195634..d90622fe4 100644
--- a/hypha/apply/funds/tests/test_admin_form.py
+++ b/hypha/apply/funds/tests/test_admin_form.py
@@ -54,6 +54,7 @@ def form_data(num_appl_forms=0, num_review_forms=0, num_determination_forms=0, n
     fund_data['workflow_name'] = workflow_for_stages(stages)
 
     form_data.update(fund_data)
+    form_data.update(approval_form='')
     return form_data
 
 
diff --git a/hypha/apply/projects/admin.py b/hypha/apply/projects/admin.py
index 711563188..a5f69ceb1 100644
--- a/hypha/apply/projects/admin.py
+++ b/hypha/apply/projects/admin.py
@@ -1,6 +1,6 @@
 from wagtail.contrib.modeladmin.options import ModelAdmin, ModelAdminGroup
 
-from .models import DocumentCategory
+from .models import DocumentCategory, ProjectApprovalForm
 
 
 class DocumentCategoryAdmin(ModelAdmin):
@@ -9,9 +9,24 @@ class DocumentCategoryAdmin(ModelAdmin):
     list_display = ('name', 'recommended_minimum',)
 
 
+class ProjectApprovalFormAdmin(ModelAdmin):
+    model = ProjectApprovalForm
+    menu_icon = 'form'
+    list_display = ('name', 'used_by',)
+
+    def used_by(self, obj):
+        rows = list()
+        for field in ('funds', 'labs',):
+            related = ', '.join(getattr(obj, f'{field}').values_list('title', flat=True))
+            if related:
+                rows.append(related)
+        return ', '.join(rows)
+
+
 class ManageAdminGoup(ModelAdminGroup):
     menu_label = 'Manage'
     menu_icon = 'folder-open-inverse'
     items = (
         DocumentCategoryAdmin,
+        ProjectApprovalFormAdmin,
     )
diff --git a/hypha/apply/projects/forms/project.py b/hypha/apply/projects/forms/project.py
index 7e0da4777..a301b3d4a 100644
--- a/hypha/apply/projects/forms/project.py
+++ b/hypha/apply/projects/forms/project.py
@@ -100,6 +100,11 @@ class ProjectApprovalForm(forms.ModelForm):
             }
 
     def save(self, *args, **kwargs):
+        self.instance.form_data = {
+            field: self.cleaned_data[field]
+            for field in self.instance.question_field_ids
+            if field in self.cleaned_data
+        }
         self.instance.user_has_updated_details = True
         return super().save(*args, **kwargs)
 
diff --git a/hypha/apply/projects/migrations/0052_projectapprovalform.py b/hypha/apply/projects/migrations/0052_projectapprovalform.py
new file mode 100644
index 000000000..0774c3373
--- /dev/null
+++ b/hypha/apply/projects/migrations/0052_projectapprovalform.py
@@ -0,0 +1,26 @@
+# Generated by Django 3.2.13 on 2022-07-06 12:34
+
+from django.db import migrations, models
+import hypha.apply.stream_forms.blocks
+import hypha.apply.stream_forms.models
+import wagtail.core.blocks
+import wagtail.core.fields
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('application_projects', '0051_remove_unnecessary_fields_from_invoice'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='ProjectApprovalForm',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=255)),
+                ('form_fields', wagtail.core.fields.StreamField([('text_markup', wagtail.core.blocks.RichTextBlock(group='Custom', label='Section text')), ('header_markup', wagtail.core.blocks.StructBlock([('heading_text', wagtail.core.blocks.CharBlock(form_classname='title', required=True)), ('size', wagtail.core.blocks.ChoiceBlock(choices=[('h2', 'H2'), ('h3', 'H3'), ('h4', 'H4')]))], group='Custom', label='Section header')), ('char', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('format', wagtail.core.blocks.ChoiceBlock(choices=[('email', 'Email'), ('url', 'URL')], label='Format', required=False)), ('default_value', wagtail.core.blocks.CharBlock(label='Default value', required=False))], group='Fields')), ('multi_inputs_char', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('format', wagtail.core.blocks.ChoiceBlock(choices=[('email', 'Email'), ('url', 'URL')], label='Format', required=False)), ('default_value', wagtail.core.blocks.CharBlock(label='Default value', required=False)), ('number_of_inputs', wagtail.core.blocks.IntegerBlock(default=2, label='Max number of inputs')), ('add_button_text', wagtail.core.blocks.CharBlock(default='Add new item', required=False))], group='Fields')), ('text', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('default_value', wagtail.core.blocks.TextBlock(label='Default value', required=False)), ('word_limit', wagtail.core.blocks.IntegerBlock(default=1000, label='Word limit'))], group='Fields')), ('number', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('default_value', wagtail.core.blocks.CharBlock(label='Default value', required=False))], group='Fields')), ('checkbox', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('default_value', wagtail.core.blocks.BooleanBlock(required=False))], group='Fields')), ('radios', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('choices', wagtail.core.blocks.ListBlock(wagtail.core.blocks.CharBlock(label='Choice')))], group='Fields')), ('dropdown', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('choices', wagtail.core.blocks.ListBlock(wagtail.core.blocks.CharBlock(label='Choice')))], group='Fields')), ('checkboxes', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('checkboxes', wagtail.core.blocks.ListBlock(wagtail.core.blocks.CharBlock(label='Checkbox')))], group='Fields')), ('date', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('default_value', wagtail.core.blocks.DateBlock(required=False))], group='Fields')), ('time', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('default_value', wagtail.core.blocks.TimeBlock(required=False))], group='Fields')), ('datetime', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('default_value', wagtail.core.blocks.DateTimeBlock(required=False))], group='Fields')), ('image', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False))], group='Fields')), ('file', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False))], group='Fields')), ('multi_file', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False))], group='Fields')), ('group_toggle', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', required=False)), ('required', wagtail.core.blocks.BooleanBlock(default=True, label='Required')), ('choices', wagtail.core.blocks.ListBlock(wagtail.core.blocks.CharBlock(label='Choice'), help_text='Please create only two choices for toggle. First choice will revel the group and the second hide it. Additional choices will be ignored.'))], group='Custom')), ('group_toggle_end', hypha.apply.stream_forms.blocks.GroupToggleEndBlock(group='Custom'))])),
+            ],
+            bases=(hypha.apply.stream_forms.models.BaseStreamForm, models.Model),
+        ),
+    ]
diff --git a/hypha/apply/projects/models/__init__.py b/hypha/apply/projects/models/__init__.py
index 2aeab4cc6..7794e9e2c 100644
--- a/hypha/apply/projects/models/__init__.py
+++ b/hypha/apply/projects/models/__init__.py
@@ -6,6 +6,7 @@ from .project import (
     DocumentCategory,
     PacketFile,
     Project,
+    ProjectApprovalForm,
     ProjectSettings,
 )
 from .report import Report, ReportConfig, ReportPrivateFiles, ReportVersion
@@ -13,6 +14,7 @@ from .vendor import BankInformation, DueDiligenceDocument, Vendor
 
 __all__ = [
     'Project',
+    'ProjectApprovalForm',
     'ProjectSettings',
     'Approval',
     'Contract',
diff --git a/hypha/apply/projects/models/project.py b/hypha/apply/projects/models/project.py
index e3c470cf5..10e5159f2 100644
--- a/hypha/apply/projects/models/project.py
+++ b/hypha/apply/projects/models/project.py
@@ -16,6 +16,7 @@ from django.dispatch.dispatcher import receiver
 from django.urls import reverse
 from django.utils import timezone
 from django.utils.translation import gettext_lazy as _
+from wagtail.admin.edit_handlers import FieldPanel, StreamFieldPanel
 from wagtail.contrib.settings.models import BaseSetting, register_setting
 from wagtail.core.fields import StreamField
 
@@ -372,6 +373,19 @@ class Project(BaseStreamForm, AccessFormData, models.Model):
     #     self.save(update_fields=['sent_to_compliance_at'])
 
 
+class ProjectApprovalForm(BaseStreamForm, models.Model):
+    name = models.CharField(max_length=255)
+    form_fields = StreamField(FormFieldsBlock())
+
+    panels = [
+        FieldPanel('name'),
+        StreamFieldPanel('form_fields'),
+    ]
+
+    def __str__(self):
+        return self.name
+
+
 @register_setting
 class ProjectSettings(BaseSetting):
     compliance_email = models.TextField("Compliance Email")
diff --git a/hypha/apply/projects/tests/factories.py b/hypha/apply/projects/tests/factories.py
index a1e710afb..8c2fc1cde 100644
--- a/hypha/apply/projects/tests/factories.py
+++ b/hypha/apply/projects/tests/factories.py
@@ -5,6 +5,7 @@ from dateutil.relativedelta import relativedelta
 from django.utils import timezone
 
 from hypha.apply.funds.tests.factories import ApplicationSubmissionFactory
+from hypha.apply.stream_forms.testing.factories import FormDataFactory, FormFieldsBlockFactory
 from hypha.apply.users.tests.factories import StaffFactory, UserFactory
 
 from ..models.payment import Invoice, InvoiceDeliverable, SupportingDocument
@@ -16,6 +17,7 @@ from ..models.project import (
     DocumentCategory,
     PacketFile,
     Project,
+    ProjectApprovalForm,
 )
 from ..models.report import Report, ReportConfig, ReportVersion
 
@@ -53,6 +55,18 @@ class DocumentCategoryFactory(factory.django.DjangoModelFactory):
         model = DocumentCategory
 
 
+class ProjectApprovalFormFactory(factory.django.DjangoModelFactory):
+    class Meta:
+        model = ProjectApprovalForm
+
+    name = factory.Faker('word')
+    form_fields = FormFieldsBlockFactory
+
+
+class ProjectApprovalFormDataFactory(FormDataFactory):
+    field_factory = FormFieldsBlockFactory
+
+
 class ProjectFactory(factory.django.DjangoModelFactory):
     submission = factory.SubFactory(ApplicationSubmissionFactory)
     user = factory.SubFactory(UserFactory)
@@ -65,6 +79,12 @@ class ProjectFactory(factory.django.DjangoModelFactory):
 
     is_locked = False
 
+    form_fields = FormFieldsBlockFactory
+    form_data = factory.SubFactory(
+        ProjectApprovalFormDataFactory,
+        form_fields=factory.SelfAttribute('..form_fields'),
+    )
+
     class Meta:
         model = Project
 
diff --git a/hypha/apply/projects/views/project.py b/hypha/apply/projects/views/project.py
index 66f2b283f..f563bd0f0 100644
--- a/hypha/apply/projects/views/project.py
+++ b/hypha/apply/projects/views/project.py
@@ -11,6 +11,7 @@ from django.shortcuts import get_object_or_404, redirect
 from django.urls import reverse, reverse_lazy
 from django.utils import timezone
 from django.utils.decorators import method_decorator
+from django.utils.functional import cached_property
 from django.utils.safestring import mark_safe
 from django.utils.translation import gettext as _
 from django.views import View
@@ -600,7 +601,35 @@ class ProjectApprovalEditView(UpdateView):
             return redirect(project)
         return super().dispatch(request, *args, **kwargs)
 
+    @cached_property
+    def approval_form(self):
+        if self.object.get_defined_fields():
+            approval_form = self.object
+        else:
+            approval_form = self.object.submission.page.specific.approval_form
+
+        return approval_form
+
+    def get_form_kwargs(self):
+        kwargs = super().get_form_kwargs()
+
+        if self.approval_form:
+            fields = self.approval_form.get_form_fields()
+        else:
+            fields = {}
+
+        kwargs['extra_fields'] = fields
+        kwargs['initial'].update(self.object.raw_data)
+        return kwargs
+
     def form_valid(self, form):
+        try:
+            form_fields = self.approval_form.form_fields
+        except AttributeError:
+            form_fields = []
+
+        form.instance.form_fields = form_fields
+
         return super().form_valid(form)
 
 
-- 
GitLab