diff --git a/opentech/apply/funds/models/mixins.py b/opentech/apply/funds/models/mixins.py new file mode 100644 index 0000000000000000000000000000000000000000..50c3c0ccdb24e4e3d3e68df66a36c8260cdb2aba --- /dev/null +++ b/opentech/apply/funds/models/mixins.py @@ -0,0 +1,99 @@ +from django.utils.text import mark_safe + +from opentech.apply.stream_forms.blocks import FormFieldBlock +from opentech.apply.utils.blocks import MustIncludeFieldBlock + + +__all__ = ['AccessFormData'] + + +class AccessFormData: + """Mixin for interacting with form data from streamfields + + requires: + - form_data > jsonfield containing the submitted data + - form_fields > streamfield containing the original form fields + + """ + + @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 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] + + @property + def question_field_ids(self): + for field_id, field in self.fields.items(): + if isinstance(field.block, FormFieldBlock): + yield field_id + + @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): + # Returns a list of the rendered answers + return [ + self.render_answer(field_id, include_question=True) + for field_id in self.question_field_ids + if field_id not in self.must_include + ] + + def output_answers(self): + # Returns a safe string of the rendered answers + return mark_safe(''.join(self.render_answers())) diff --git a/opentech/apply/funds/models/submissions.py b/opentech/apply/funds/models/submissions.py index 3f6bf83a248f55964919a74bc86d8502db709a72..93d0c700be73fe95dadfe2485ef6900333c8adcf 100644 --- a/opentech/apply/funds/models/submissions.py +++ b/opentech/apply/funds/models/submissions.py @@ -12,7 +12,7 @@ from django.db.models import ObjectDoesNotExist from django.db.models.expressions import RawSQL, OrderBy from django.dispatch import receiver from django.urls import reverse -from django.utils.text import mark_safe, slugify +from django.utils.text import slugify from django_fsm import can_proceed, FSMField, transition, RETURN_VALUE from django_fsm.signals import post_transition @@ -21,11 +21,11 @@ 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 FormFieldBlock, UploadableMediaBlock +from opentech.apply.stream_forms.blocks import UploadableMediaBlock from opentech.apply.stream_forms.models import BaseStreamForm -from opentech.apply.utils.blocks import MustIncludeFieldBlock +from .mixins import AccessFormData from .utils import LIMIT_TO_STAFF, LIMIT_TO_STAFF_AND_REVIEWERS, WorkflowHelpers from ..blocks import ApplicationCustomFormFieldsBlock, REQUIRED_BLOCK_NAMES from ..workflow import ( @@ -228,6 +228,7 @@ class ApplicationSubmissionMetaclass(AddTransitions): class ApplicationSubmission( WorkflowHelpers, BaseStreamForm, + AccessFormData, AbstractFormSubmission, metaclass=ApplicationSubmissionMetaclass, ): @@ -544,86 +545,7 @@ class ApplicationSubmission( return form_data - @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 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] - - @property - def question_field_ids(self): - for field_id, field in self.fields.items(): - if isinstance(field.block, FormFieldBlock): - yield field_id - - @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): - return [ - self.render_answer(field_id, include_question=True) - for field_id in self.question_field_ids - if field_id not in self.must_include - ] - - def output_answers(self): - return mark_safe(''.join(self.render_answers())) - + # Template methods for metaclass def _get_REQUIRED_display(self, name): return self.render_answer(name) diff --git a/opentech/apply/review/models.py b/opentech/apply/review/models.py index b55297626f558e92c6c2cff60e81ee621be5b092..8bddc8e5a5163c7f258b08a15a723b97f978b495 100644 --- a/opentech/apply/review/models.py +++ b/opentech/apply/review/models.py @@ -9,6 +9,7 @@ from django.utils.safestring import mark_safe from wagtail.admin.edit_handlers import FieldPanel, StreamFieldPanel from wagtail.core.fields import StreamField +from opentech.apply.funds.models.mixins import AccessFormData from opentech.apply.review.options import YES, NO, MAYBE, RECOMMENDATION_CHOICES from opentech.apply.stream_forms.models import BaseStreamForm from opentech.apply.users.models import User @@ -68,7 +69,7 @@ class ReviewQuerySet(models.QuerySet): return MAYBE -class Review(BaseStreamForm, models.Model): +class Review(BaseStreamForm, AccessFormData, models.Model): submission = models.ForeignKey('funds.ApplicationSubmission', on_delete=models.CASCADE, related_name='reviews') revision = models.ForeignKey('funds.ApplicationRevision', on_delete=models.SET_NULL, related_name='reviews', null=True) author = models.ForeignKey( @@ -101,25 +102,6 @@ class Review(BaseStreamForm, models.Model): def __repr__(self): return f'<{self.__class__.__name__}: {str(self.form_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)) - @receiver(post_save, sender=Review) def update_submission_reviewers_list(sender, **kwargs):