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 0000000000000000000000000000000000000000..7f03ba5508b3d46cdd69c4e56958bda154d78a22 --- /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 ef24ed5599e23b91357338a0fc0f383fd2ee7c9e..13837638bc5b5e5d94837eeab6297f921f8ee35b 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 9da35a9b8fbf14a428183e84ddfc1036d963ee95..59727aa6127e314069bd332d97b752d419ba12fa 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 80119563409e97f5ae1f4c905c22da621af7579d..d90622fe487b524c23307f7d44f2c1ee8760ba97 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 711563188a21bdd91c2e29ea110956626a556f9e..a5f69ceb13a96bdd8bfc04b091ed4349be8d10d3 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 7e0da4777bf2d0825fab2523746984f1a4caff3e..a301b3d4ac8673207520990ba88cd22f5b12e6e4 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 0000000000000000000000000000000000000000..0774c3373e944c1d5d2408be33556fedc6c007f4 --- /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 2aeab4cc6bd89e11b3468d39c662d35c8381deb7..7794e9e2cec0dd0c1d67c447375c54744f5b528a 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 e3c470cf519a62352fb126278bf48d9becb95453..10e5159f203d946c80dddf3c53f09e14ba938c6a 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 a1e710afb1f5836c98606f814241e4dafc1d40db..8c2fc1cde1ccc69b7d1acb42fce6a22c2294857a 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 66f2b283f5a1065fae3e26fcbf7b8858f73f1335..f563bd0f01d35caafd620ed98249028ac2031eb3 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)