diff --git a/opentech/apply/funds/blocks.py b/opentech/apply/funds/blocks.py index 416234725e7564b5441a3f697a47700c1f195a02..ccc4bb0db34a97b1c08c799cc3fc907c41e28f6d 100644 --- a/opentech/apply/funds/blocks.py +++ b/opentech/apply/funds/blocks.py @@ -1,3 +1,5 @@ +import json + from django import forms from django.utils.translation import ugettext_lazy as _ @@ -15,6 +17,9 @@ class TitleBlock(ApplicationMustIncludeFieldBlock): name = 'title' description = 'The title of the project' + class Meta: + icon = 'tag' + class ValueBlock(ApplicationMustIncludeFieldBlock): name = 'value' @@ -41,6 +46,18 @@ class AddressFieldBlock(ApplicationMustIncludeFieldBlock): label = _('Address') icon = 'home' + def format_data(self, data): + # Based on the fields listed in addressfields/widgets.py + order_fields = [ + 'thoroughfare', 'premise', 'localityname', 'administrativearea', 'postalcode', 'country' + ] + address = json.loads(data) + return ', '.join( + address[field] + for field in order_fields + if address[field] + ) + class FullNameBlock(ApplicationMustIncludeFieldBlock): name = 'full_name' @@ -50,6 +67,40 @@ class FullNameBlock(ApplicationMustIncludeFieldBlock): icon = 'user' +class DurationBlock(ApplicationMustIncludeFieldBlock): + name = 'duration' + description = 'Duration' + + DURATION_OPTIONS = { + 1: "1 month", + 2: "2 months", + 3: "3 months", + 4: "4 months", + 5: "5 months", + 6: "6 months", + 7: "7 months", + 8: "8 months", + 9: "9 months", + 10: "10 months", + 11: "11 months", + 12: "12 months", + 18: "18 months", + 24: "24 months", + } + field_class = forms.ChoiceField + + def get_field_kwargs(self, *args, **kwargs): + field_kwargs = super().get_field_kwargs(*args, **kwargs) + field_kwargs['choices'] = self.DURATION_OPTIONS.items() + return field_kwargs + + def format_data(self, data): + return self.DURATION_OPTIONS[int(data)] + + class Meta: + icon = 'date' + + class ApplicationCustomFormFieldsBlock(CustomFormFieldsBlock, FormFieldsBlock): category = CategoryQuestionBlock(group=_('Custom')) required_blocks = ApplicationMustIncludeFieldBlock.__subclasses__() diff --git a/opentech/apply/funds/migrations/0040_add_duration_stream_to_streamfield_definition.py b/opentech/apply/funds/migrations/0040_add_duration_stream_to_streamfield_definition.py new file mode 100644 index 0000000000000000000000000000000000000000..8266e50d7546d0f7f7a0fcca1bf5b338965f08d7 --- /dev/null +++ b/opentech/apply/funds/migrations/0040_add_duration_stream_to_streamfield_definition.py @@ -0,0 +1,27 @@ +# Generated by Django 2.0.2 on 2018-08-10 08:28 + +from django.db import migrations +import opentech.apply.categories.blocks +import wagtail.core.blocks +import wagtail.core.blocks.static_block +import wagtail.core.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('funds', '0039_add_rfps_and_sealed_rounds'), + ] + + operations = [ + migrations.AlterField( + model_name='applicationform', + name='form_fields', + field=wagtail.core.fields.StreamField((('text_markup', wagtail.core.blocks.RichTextBlock(group='Other', label='Paragraph')), ('char', wagtail.core.blocks.StructBlock((('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', 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')), ('text', wagtail.core.blocks.StructBlock((('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('default_value', wagtail.core.blocks.TextBlock(label='Default value', required=False))), 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)), ('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)), ('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)), ('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)), ('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)), ('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)), ('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)), ('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)), ('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)), ('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)), ('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)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False))), group='Fields')), ('rich_text', wagtail.core.blocks.StructBlock((('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('default_value', wagtail.core.blocks.TextBlock(label='Default value', required=False))), group='Fields')), ('category', wagtail.core.blocks.StructBlock((('field_label', wagtail.core.blocks.CharBlock(help_text='Leave blank to use the default Category label', label='Label', required=False)), ('help_text', wagtail.core.blocks.TextBlock(label='Leave blank to use the default Category help text', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('category', opentech.apply.categories.blocks.ModelChooserBlock('categories.Category')), ('multi', wagtail.core.blocks.BooleanBlock(label='Multi select', required=False))), group='Custom')), ('title', wagtail.core.blocks.StructBlock((('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('info', wagtail.core.blocks.static_block.StaticBlock())), group='Required')), ('value', wagtail.core.blocks.StructBlock((('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('info', wagtail.core.blocks.static_block.StaticBlock())), group='Required')), ('email', wagtail.core.blocks.StructBlock((('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('info', wagtail.core.blocks.static_block.StaticBlock())), group='Required')), ('address', wagtail.core.blocks.StructBlock((('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('info', wagtail.core.blocks.static_block.StaticBlock())), group='Required')), ('full_name', wagtail.core.blocks.StructBlock((('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('info', wagtail.core.blocks.static_block.StaticBlock())), group='Required')), ('duration', wagtail.core.blocks.StructBlock((('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('info', wagtail.core.blocks.static_block.StaticBlock())), group='Required')))), + ), + migrations.AlterField( + model_name='applicationsubmission', + name='form_fields', + field=wagtail.core.fields.StreamField((('text_markup', wagtail.core.blocks.RichTextBlock(group='Other', label='Paragraph')), ('char', wagtail.core.blocks.StructBlock((('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', 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')), ('text', wagtail.core.blocks.StructBlock((('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('default_value', wagtail.core.blocks.TextBlock(label='Default value', required=False))), 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)), ('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)), ('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)), ('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)), ('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)), ('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)), ('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)), ('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)), ('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)), ('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)), ('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)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False))), group='Fields')), ('rich_text', wagtail.core.blocks.StructBlock((('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('default_value', wagtail.core.blocks.TextBlock(label='Default value', required=False))), group='Fields')), ('category', wagtail.core.blocks.StructBlock((('field_label', wagtail.core.blocks.CharBlock(help_text='Leave blank to use the default Category label', label='Label', required=False)), ('help_text', wagtail.core.blocks.TextBlock(label='Leave blank to use the default Category help text', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('category', opentech.apply.categories.blocks.ModelChooserBlock('categories.Category')), ('multi', wagtail.core.blocks.BooleanBlock(label='Multi select', required=False))), group='Custom')), ('title', wagtail.core.blocks.StructBlock((('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('info', wagtail.core.blocks.static_block.StaticBlock())), group='Required')), ('value', wagtail.core.blocks.StructBlock((('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('info', wagtail.core.blocks.static_block.StaticBlock())), group='Required')), ('email', wagtail.core.blocks.StructBlock((('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('info', wagtail.core.blocks.static_block.StaticBlock())), group='Required')), ('address', wagtail.core.blocks.StructBlock((('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('info', wagtail.core.blocks.static_block.StaticBlock())), group='Required')), ('full_name', wagtail.core.blocks.StructBlock((('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('info', wagtail.core.blocks.static_block.StaticBlock())), group='Required')), ('duration', wagtail.core.blocks.StructBlock((('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('info', wagtail.core.blocks.static_block.StaticBlock())), group='Required')))), + ), + ] diff --git a/opentech/apply/funds/models/submissions.py b/opentech/apply/funds/models/submissions.py index 811ee371bb330642fd2b52d926375fca35917198..65ff60084487082b91d884ae7117ddb610dc558d 100644 --- a/opentech/apply/funds/models/submissions.py +++ b/opentech/apply/funds/models/submissions.py @@ -1,4 +1,5 @@ import os +from functools import partialmethod from django.conf import settings from django.contrib.auth import get_user_model @@ -20,7 +21,7 @@ from wagtail.core.fields import StreamField from wagtail.contrib.forms.models import AbstractFormSubmission from opentech.apply.activity.messaging import messenger, MESSAGES -from opentech.apply.stream_forms.blocks import UploadableMediaBlock +from opentech.apply.stream_forms.blocks import FormFieldBlock, UploadableMediaBlock from opentech.apply.stream_forms.models import BaseStreamForm from opentech.apply.utils.blocks import MustIncludeFieldBlock @@ -192,7 +193,44 @@ class AddTransitions(models.base.ModelBase): return super().__new__(cls, name, bases, attrs, **kwargs) -class ApplicationSubmission(WorkflowHelpers, BaseStreamForm, AbstractFormSubmission, metaclass=AddTransitions): +class ApplicationSubmissionMetaclass(AddTransitions): + def __new__(cls, name, bases, attrs, **kwargs): + cls = super().__new__(cls, name, bases, attrs, **kwargs) + + # We want to access the redered display of the required fields. + # Treat in similar way to django's get_FIELD_display + for required_name in REQUIRED_BLOCK_NAMES: + partial_method_name = f'_{required_name}_method' + # We need to generate the partial method and the wrap it in property so + # we can access the required fields like normal fields. e.g. self.title + # Partial method requires __get__ to be called in order to bind it to the + # class properly this is using the <name> -> _<name>_method -> _get_REQUIRED_value + # call chain which instantiates each method correctly at the cost of an extra + # lookup + setattr( + cls, + partial_method_name, + partialmethod(cls._get_REQUIRED_value, name=required_name), + ) + setattr( + cls, + f'{required_name}', + property(getattr(cls, partial_method_name)), + ) + setattr( + cls, + f'get_{required_name}_display', + partialmethod(cls._get_REQUIRED_display, name=required_name), + ) + return cls + + +class ApplicationSubmission( + WorkflowHelpers, + BaseStreamForm, + AbstractFormSubmission, + metaclass=ApplicationSubmissionMetaclass, +): field_template = 'funds/includes/submission_field.html' form_data = JSONField(encoder=DjangoJSONEncoder) @@ -386,14 +424,6 @@ class ApplicationSubmission(WorkflowHelpers, BaseStreamForm, AbstractFormSubmiss self.ensure_user_has_account() self.process_file_data() - @property - def must_include(self): - return { - field.block.name: field.id - for field in self.form_fields - if isinstance(field.block, MustIncludeFieldBlock) - } - def process_form_data(self): for field_name, field_id in self.must_include.items(): response = self.form_data.pop(field_id, None) @@ -479,36 +509,11 @@ class ApplicationSubmission(WorkflowHelpers, BaseStreamForm, AbstractFormSubmiss def can_have_determination(self): return self.in_determination_phase and not self.has_determination - @property - def raw_data(self): - data = self.form_data.copy() - for field_name, field_id in self.must_include.items(): - response = data.pop(field_name) - data[field_id] = response - return data - - def data_and_fields(self): - for stream_value in self.form_fields: - try: - data = self.form_data[stream_value.id] - except KeyError: - pass # It was a named field or a paragraph - else: - yield data, stream_value - - @property - def fields(self): - return [ - field.render(context={'data': data}) - for data, field in self.data_and_fields() - ] - - def render_answers(self): - return mark_safe(''.join(self.fields)) - def prepare_search_values(self): - for data, stream in self.data_and_fields(): - value = stream.block.get_searchable_content(stream.value, data) + for field_id in self.question_field_ids: + field = self.field(field_id) + data = self.data(field_id) + value = field.block.get_searchable_content(field.value, data) if value: if isinstance(value, list): yield ', '.join(value) @@ -519,8 +524,19 @@ class ApplicationSubmission(WorkflowHelpers, BaseStreamForm, AbstractFormSubmiss for field in ['email', 'title']: yield getattr(self, field) + def get_absolute_url(self): + return reverse('funds:submissions:detail', args=(self.id,)) + + def __str__(self): + return f'{self.title} from {self.full_name} for {self.page.title}' + + def __repr__(self): + return f'<{self.__class__.__name__}: {self.user}, {self.round}, {self.page}>' + + # Methods for accessing data on the submission + def get_data(self): - # Updated for JSONField + # Updated for JSONField - Not used but base get_data will error form_data = self.form_data.copy() form_data.update({ 'submit_time': self.submit_time, @@ -528,21 +544,89 @@ class ApplicationSubmission(WorkflowHelpers, BaseStreamForm, AbstractFormSubmiss return form_data - def get_absolute_url(self): - return reverse('funds:submissions:detail', args=(self.id,)) + @property + def raw_data(self): + # Returns the data mapped by field id instead of the data stored using the must include + # values + data = self.form_data.copy() + for field_name, field_id in self.must_include.items(): + response = data.pop(field_name) + data[field_id] = response + return data + + def field(self, id): + try: + return self.fields[id] + except KeyError as e: + try: + actual_id = self.must_include[id] + except KeyError: + raise e + else: + return self.fields[actual_id] - def __getattribute__(self, item): - # __getattribute__ allows correct error handling from django compared to __getattr__ - # fall back to values defined on the data - if item in REQUIRED_BLOCK_NAMES: - return self.get_data()[item] - return super().__getattribute__(item) + def data(self, id): + try: + return self.form_data[id] + except KeyError as e: + try: + transposed_must_include = {v: k for k, v in self.must_include.items()} + actual_id = transposed_must_include[id] + except KeyError: + # We have most likely progressed application forms so the data isnt in form_data + return None + else: + return self.form_data[actual_id] - def __str__(self): - return f'{self.title} from {self.full_name} for {self.page.title}' + @property + def question_field_ids(self): + for field_id, field in self.fields.items(): + if isinstance(field.block, FormFieldBlock): + yield field_id - def __repr__(self): - return f'<{self.__class__.__name__}: {self.user}, {self.round}, {self.page}>' + @property + def raw_fields(self): + # Field ids to field class mapping - similar to raw_data + return { + field.id: field + for field in self.form_fields + } + + @property + def fields(self): + # ALl fields on the application + fields = self.raw_fields.copy() + for field_name, field_id in self.must_include.items(): + response = fields.pop(field_id) + fields[field_name] = response + return fields + + @property + def must_include(self): + return { + field.block.name: field.id + for field in self.form_fields + if isinstance(field.block, MustIncludeFieldBlock) + } + + def render_answer(self, field_id, include_question=False): + field = self.field(field_id) + data = self.data(field_id) + return field.render(context={'data': data, 'include_question': include_question}) + + def render_answers(self): + answers = [ + self.render_answer(field_id, include_question=True) + for field_id in self.question_field_ids + if field_id not in self.must_include + ] + return mark_safe(''.join(answers)) + + def _get_REQUIRED_display(self, name): + return self.render_answer(name) + + def _get_REQUIRED_value(self, name): + return self.form_data[name] @receiver(post_transition, sender=ApplicationSubmission) diff --git a/opentech/apply/funds/templates/funds/includes/rendered_answers.html b/opentech/apply/funds/templates/funds/includes/rendered_answers.html index e5b7f88b2266a90a71dc67d714ee0dbfa40e05b9..e056156ba1d7ce22e34f4e22122b179d12ed17c0 100644 --- a/opentech/apply/funds/templates/funds/includes/rendered_answers.html +++ b/opentech/apply/funds/templates/funds/includes/rendered_answers.html @@ -8,7 +8,7 @@ <div> <h5>Project Duration</h5> - <p>{{ object.value }}</p> + <p>{{ object.get_duration_display }}</p> </div> <div> @@ -20,6 +20,10 @@ <h5>Email</h5> <p>{{ object.email }}</p> </div> + <div class="grid__cell--span-two"> + <h5>Address</h5> + <p>{{ object.get_address_display }}</p> + </div> </div> <div class="rich-text rich-text--answers"> {{ object.render_answers }} diff --git a/opentech/apply/funds/tests/factories/blocks.py b/opentech/apply/funds/tests/factories/blocks.py index 8d378e36123893fe29af73ade86a4f2c346fc4d8..77cc38d4c0f70518bdb3cb58e07eacdf48835dbc 100644 --- a/opentech/apply/funds/tests/factories/blocks.py +++ b/opentech/apply/funds/tests/factories/blocks.py @@ -2,9 +2,17 @@ import random import factory from opentech.apply.funds import blocks -from opentech.apply.stream_forms.testing.factories import FormFieldBlockFactory, CharFieldBlockFactory, \ - NumberFieldBlockFactory, RadioFieldBlockFactory, ImageFieldBlockFactory, FileFieldBlockFactory, \ - MultiFileFieldBlockFactory, StreamFieldUUIDFactory +from opentech.apply.stream_forms.testing.factories import ( + CharFieldBlockFactory, + FileFieldBlockFactory, + FormFieldBlockFactory, + ImageFieldBlockFactory, + MultiFileFieldBlockFactory, + NumberFieldBlockFactory, + RadioFieldBlockFactory, + ParagraphBlockFactory, + StreamFieldUUIDFactory, +) from opentech.apply.utils.testing.factories import RichTextFieldBlockFactory __all__ = ['CustomFormFieldsFactory', 'TitleBlockFactory', 'EmailBlockFactory', 'FullNameBlockFactory', 'ValueFieldBlockFactory'] @@ -31,6 +39,16 @@ class FullNameBlockFactory(FormFieldBlockFactory): model = blocks.FullNameBlock +class DurationBlockFactory(FormFieldBlockFactory): + class Meta: + model = blocks.DurationBlock + + @classmethod + def make_answer(cls, params=dict()): + choices = list(blocks.DurationBlock.DURATION_OPTIONS.keys()) + return random.choice(choices) + + class ValueFieldBlockFactory(FormFieldBlockFactory): class Meta: model = blocks.ValueBlock @@ -41,6 +59,7 @@ class ValueFieldBlockFactory(FormFieldBlockFactory): CustomFormFieldsFactory = StreamFieldUUIDFactory({ + 'duration': DurationBlockFactory, 'title': TitleBlockFactory, 'value': ValueFieldBlockFactory, 'email': EmailBlockFactory, @@ -52,4 +71,5 @@ CustomFormFieldsFactory = StreamFieldUUIDFactory({ 'image': ImageFieldBlockFactory, 'file': FileFieldBlockFactory, 'multi_file': MultiFileFieldBlockFactory, + 'text_markup': ParagraphBlockFactory, }) diff --git a/opentech/apply/funds/tests/factories/models.py b/opentech/apply/funds/tests/factories/models.py index 286c340cd1fd4a05721107d15625c0a5aa5ec4cc..cffdc36f173ab0db015ca56f1b1d5ebc88774158 100644 --- a/opentech/apply/funds/tests/factories/models.py +++ b/opentech/apply/funds/tests/factories/models.py @@ -1,4 +1,3 @@ -from collections import defaultdict import datetime import factory @@ -42,25 +41,6 @@ __all__ = [ ] -def build_form(data, prefix=''): - if prefix: - prefix += '__' - - extras = defaultdict(dict) - for key, value in data.items(): - if 'form_fields' in key: - _, field, attr = key.split('__') - extras[field][attr] = value - - form_fields = {} - for i, field in enumerate(blocks.CustomFormFieldsFactory.factories): - form_fields[f'{prefix}form_fields__{i}__{field}__'] = '' - for attr, value in extras[field].items(): - form_fields[f'{prefix}form_fields__{i}__{field}__{attr}'] = value - - return form_fields - - class FundTypeFactory(wagtail_factories.PageFactory): class Meta: model = FundType @@ -89,16 +69,14 @@ class FundTypeFactory(wagtail_factories.PageFactory): @factory.post_generation def forms(self, create, extracted, **kwargs): if create: - from opentech.apply.review.tests.factories.models import build_form as review_build_form, ReviewFormFactory - fields = build_form(kwargs, prefix='form') - review_fields = review_build_form(kwargs) + from opentech.apply.review.tests.factories.models import ReviewFormFactory for _ in self.workflow.stages: # Generate a form based on all defined fields on the model ApplicationBaseFormFactory( application=self, - **fields, + **kwargs, ) - ReviewFormFactory(**review_fields) + ReviewFormFactory(**kwargs) class RequestForPartnersFactory(FundTypeFactory): @@ -144,12 +122,11 @@ class RoundFactory(wagtail_factories.PageFactory): @factory.post_generation def forms(self, create, extracted, **kwargs): if create: - fields = build_form(kwargs, prefix='form') for _ in self.workflow.stages: # Generate a form based on all defined fields on the model RoundBaseFormFactory( round=self, - **fields, + **kwargs, ) @@ -184,12 +161,11 @@ class LabFactory(wagtail_factories.PageFactory): @factory.post_generation def forms(self, create, extracted, **kwargs): if create: - fields = build_form(kwargs, prefix='form') for _ in self.workflow.stages: # Generate a form based on all defined fields on the model LabBaseFormFactory( lab=self, - **fields, + **kwargs, ) @@ -236,11 +212,6 @@ class ApplicationSubmissionFactory(factory.DjangoModelFactory): if create and reviewers: self.reviewers.set(reviewers) - @classmethod - def _generate(cls, strat, params): - params.update(**build_form(params)) - return super()._generate(strat, params) - class SealedSubmissionFactory(ApplicationSubmissionFactory): page = factory.SubFactory(RequestForPartnersFactory) diff --git a/opentech/apply/funds/tests/test_models.py b/opentech/apply/funds/tests/test_models.py index 44b07e3891733bd7d965a3a58d94d785edb59433..9b21854c42f8127783fd6889788ba0b09b6a4293 100644 --- a/opentech/apply/funds/tests/test_models.py +++ b/opentech/apply/funds/tests/test_models.py @@ -172,7 +172,7 @@ class TestRoundModelWorkflowAndForms(TestCase): del self.round.parent_page form = self.round.forms.first().form # Not ideal, would prefer better way to create the stream values - new_field = CustomFormFieldsFactory.generate(None, {'0__email__': ''}) + new_field = CustomFormFieldsFactory.generate(None, {}) form.form_fields = new_field form.save() for round_form, fund_form in itertools.zip_longest(self.round.forms.all(), self.fund.forms.all()): @@ -204,7 +204,8 @@ class TestFormSubmission(TestCase): page = page or self.round_page fields = page.get_form_fields() - data = {k: v for k, v in zip(fields, ['project', 0, email, name])} + # This needs to match the order of the fields defined on the form factory + data = {k: v for k, v in zip(fields, [1, 'project', 0, email, name])} request = make_request(user, data, method='post', site=self.site) try: @@ -419,3 +420,28 @@ class TestApplicationSubmission(TestCase): submission.form_data = {'title': title} submission.create_revision(draft=True) self.assertEqual(submission.revisions.count(), 2) + + +class TestSubmissionRenderMethods(TestCase): + def test_must_include_not_included_in_answers(self): + submission = ApplicationSubmissionFactory() + answers = submission.render_answers() + for name in submission.must_include: + field = submission.field(name) + self.assertNotIn(field.value['field_label'], answers) + + def test_normal_answers_included_in_answers(self): + submission = ApplicationSubmissionFactory() + answers = submission.render_answers() + for field_name in submission.question_field_ids: + if field_name not in submission.must_include: + field = submission.field(field_name) + self.assertIn(field.value['field_label'], answers) + + def test_paragraph_not_rendered_in_answers(self): + rich_text_label = 'My rich text label!' + submission = ApplicationSubmissionFactory( + form_fields__text_markup__value=rich_text_label + ) + answers = submission.render_answers() + self.assertNotIn(rich_text_label, answers) diff --git a/opentech/apply/review/tests/factories/models.py b/opentech/apply/review/tests/factories/models.py index 2be83c8a6bcd513a694b4cc9bec23f03c7f4711a..79a82eb4b6b3052a4a02bb00dde3aff66d3600b4 100644 --- a/opentech/apply/review/tests/factories/models.py +++ b/opentech/apply/review/tests/factories/models.py @@ -1,5 +1,3 @@ -from collections import defaultdict - import factory from opentech.apply.funds.models.forms import ApplicationBaseReviewForm @@ -13,27 +11,9 @@ from ...views import get_fields_for_stage from . import blocks -__all__ = ['ReviewFactory', 'ReviewFormFactory', 'ApplicationBaseReviewFormFactory', - 'ReviewFundTypeFactory', 'ReviewApplicationSubmissionFactory'] - - -def build_form(data, prefix=''): - if prefix: - prefix += '__' - - extras = defaultdict(dict) - for key, value in data.items(): - if 'form_fields' in key: - _, field, attr = key.split('__') - extras[field][attr] = value - - form_fields = {} - for i, field in enumerate(blocks.ReviewFormFieldsFactory.factories): - form_fields[f'{prefix}form_fields__{i}__{field}__'] = '' - for attr, value in extras[field].items(): - form_fields[f'{prefix}form_fields__{i}__{field}__{attr}'] = value - - return form_fields +__all__ = ['ReviewFactory', 'ReviewFormFactory', + 'ApplicationBaseReviewFormFactory', 'ReviewFundTypeFactory', + 'ReviewApplicationSubmissionFactory'] class ReviewFormDataFactory(factory.DictFactory, metaclass=AddFormFieldsMetaclass): @@ -64,7 +44,11 @@ class ReviewFactory(factory.DjangoModelFactory): submission = factory.SubFactory(ApplicationSubmissionFactory) author = factory.SubFactory(StaffFactory) form_fields = blocks.ReviewFormFieldsFactory - form_data = factory.SubFactory(ReviewFormDataFactory, form_fields=factory.SelfAttribute('..form_fields'), submission=factory.SelfAttribute('..submission')) + form_data = factory.SubFactory( + ReviewFormDataFactory, + form_fields=factory.SelfAttribute('..form_fields'), + submission=factory.SelfAttribute('..submission'), + ) is_draft = False recommendation = NO score = 0 @@ -89,12 +73,11 @@ class ReviewFundTypeFactory(FundTypeFactory): @factory.post_generation def review_forms(self, create, extracted, **kwargs): if create: - fields = build_form(kwargs, prefix='form') for _ in self.workflow.stages: # Generate a form based on all defined fields on the model ApplicationBaseReviewFormFactory( application=self, - **fields + **kwargs ) diff --git a/opentech/apply/stream_forms/blocks.py b/opentech/apply/stream_forms/blocks.py index 903f232283e80dfb22b0fa149d045c8c65b5f344..77692d430db347d4f637f0761c874160f43d6726 100644 --- a/opentech/apply/stream_forms/blocks.py +++ b/opentech/apply/stream_forms/blocks.py @@ -50,9 +50,20 @@ class FormFieldBlock(StructBlock): return self.get_field_class(struct_value)( **self.get_field_kwargs(struct_value)) + def get_context(self, value, parent_context): + context = super().get_context(value, parent_context) + parent_context['data'] = self.format_data(parent_context['data']) or self.no_response() + return context + def get_searchable_content(self, value, data): return str(data) + def format_data(self, data): + return data + + def no_responose(self): + return "No response" + class OptionalFormFieldBlock(FormFieldBlock): required = BooleanBlock(label=_('Required'), required=False) @@ -126,6 +137,9 @@ class CheckboxFieldBlock(FormFieldBlock): def get_searchable_content(self, value, data): return None + def no_responose(self): + return False + class RadioButtonsFieldBlock(OptionalFormFieldBlock): choices = ListBlock(CharBlock(label=_('Choice'))) diff --git a/opentech/apply/stream_forms/templates/stream_forms/render_field.html b/opentech/apply/stream_forms/templates/stream_forms/render_field.html index 049375a32c6fc5d22bf2a4516b101452ffbbb17c..8826fee6c0450155b406543ff9f1afd5d40e18fd 100644 --- a/opentech/apply/stream_forms/templates/stream_forms/render_field.html +++ b/opentech/apply/stream_forms/templates/stream_forms/render_field.html @@ -1,4 +1,10 @@ +{% if include_question %} <div> <h5>{{ value.field_label }}</h5> - <div>{% block data_display %}{{ data|default:"No response" }}{% endblock %}</div> +{% endif %} + + <div>{% block data_display %}{{ data }}{% endblock %}</div> + +{% if include_question %} </div> +{% endif %} diff --git a/opentech/apply/stream_forms/testing/factories.py b/opentech/apply/stream_forms/testing/factories.py index 1d03a5670b7a3c7978245cb81a10ae1a8698949b..dfccb6bf957c067335c2f011f7f7d4a29025e96c 100644 --- a/opentech/apply/stream_forms/testing/factories.py +++ b/opentech/apply/stream_forms/testing/factories.py @@ -5,14 +5,16 @@ import uuid from django.core.files.uploadedfile import InMemoryUploadedFile import factory -from wagtail.core.blocks import CharBlock +from wagtail.core.blocks import RichTextBlock +from wagtail.core.rich_text import RichText import wagtail_factories from opentech.apply.stream_forms import blocks as stream_blocks -__all__ = ['CharBlockFactory', 'FormFieldBlockFactory', 'CharFieldBlockFactory', 'NumberFieldBlockFactory', - 'RadioFieldBlockFactory', 'UploadableMediaFactory', 'ImageFieldBlockFactory', 'FileFieldBlockFactory', - 'MultiFileFieldBlockFactory'] +__all__ = ['FormFieldBlockFactory', 'CharFieldBlockFactory', + 'NumberFieldBlockFactory', 'RadioFieldBlockFactory', + 'UploadableMediaFactory', 'ImageFieldBlockFactory', + 'FileFieldBlockFactory', 'MultiFileFieldBlockFactory'] class AnswerFactory(factory.Factory): @@ -28,6 +30,7 @@ class AddFormFieldsMetaclass(factory.base.FactoryMetaClass): wrapped_factories = { k: factory.SubFactory(AnswerFactory, sub_factory=v) for k, v in field_factory.factories.items() + if issubclass(v, FormFieldBlockFactory) } attrs.update(wrapped_factories) return super().__new__(mcs, class_name, bases, attrs) @@ -63,13 +66,20 @@ class FormDataFactory(factory.Factory, metaclass=AddFormFieldsMetaclass): return form_data -class CharBlockFactory(wagtail_factories.blocks.BlockFactory): +class ParagraphBlockFactory(wagtail_factories.blocks.BlockFactory): class Meta: - model = CharBlock + model = RichTextBlock + + @classmethod + def _create(cls, model_class, value): + value = RichText(value) + return super()._create(model_class, value) class FormFieldBlockFactory(wagtail_factories.StructBlockFactory): - default_value = factory.Faker('word') + default_value = factory.Faker('sentence') + field_label = factory.Faker('sentence') + help_text = factory.LazyAttribute(lambda o: str(o._Resolver__step.builder.factory_meta.model)) class Meta: model = stream_blocks.FormFieldBlock @@ -138,8 +148,7 @@ class MultiFileFieldBlockFactory(UploadableMediaFactory): class StreamFieldUUIDFactory(wagtail_factories.StreamFieldFactory): def generate(self, step, params): - if not params: - params = self.build_form(params) + params = self.build_form(params) blocks = super().generate(step, params) ret_val = list() # Convert to JSON so we can add id before create @@ -151,10 +160,17 @@ class StreamFieldUUIDFactory(wagtail_factories.StreamFieldFactory): def build_form(self, data): extras = defaultdict(dict) + for field, value in data.items(): + # we dont care about position + name, attr = field.split('__') + extras[name] = {attr: value} form_fields = {} for i, field in enumerate(self.factories): - form_fields[f'{i}__{field}__'] = '' + if field == 'text_markup': + pass + else: + form_fields[f'{i}__{field}__'] = '' for attr, value in extras[field].items(): form_fields[f'{i}__{field}__{attr}'] = value diff --git a/opentech/apply/utils/blocks.py b/opentech/apply/utils/blocks.py index 3df0908995fc3a92ed3460762bafac95f9f6d24d..ee5aac70634505e766b063a2678858db0c5421ba 100644 --- a/opentech/apply/utils/blocks.py +++ b/opentech/apply/utils/blocks.py @@ -36,7 +36,7 @@ class RichTextFieldBlock(TextFieldBlock): icon = 'form' def get_searchable_content(self, value, data): - return bleach.clean(data, tags=[], strip=True) + return bleach.clean(data or '', tags=[], strip=True) class CustomFormFieldsBlock(StreamBlock): diff --git a/opentech/static_src/src/sass/apply/components/_grid.scss b/opentech/static_src/src/sass/apply/components/_grid.scss index 3a633983b84a1e8c79b415e2b3aba95347d5fb45..7756eedd427bee539deceaa5ac75a03d193ea49f 100644 --- a/opentech/static_src/src/sass/apply/components/_grid.scss +++ b/opentech/static_src/src/sass/apply/components/_grid.scss @@ -86,5 +86,13 @@ margin: 0; } } + + &__cell { + &--span-two{ + @include media-query(mob-landscape) { + grid-column: auto / span 2; + } + } + } } }