From f74e460b8b5897b16cb80661c7f159b681294193 Mon Sep 17 00:00:00 2001
From: Todd Dembrey <todd.dembrey@torchbox.com>
Date: Wed, 15 Aug 2018 14:22:22 +0100
Subject: [PATCH] Update reviews to use the improved field access methods

---
 opentech/apply/funds/models/mixins.py      | 99 ++++++++++++++++++++++
 opentech/apply/funds/models/submissions.py | 88 ++-----------------
 opentech/apply/review/models.py            | 22 +----
 3 files changed, 106 insertions(+), 103 deletions(-)
 create mode 100644 opentech/apply/funds/models/mixins.py

diff --git a/opentech/apply/funds/models/mixins.py b/opentech/apply/funds/models/mixins.py
new file mode 100644
index 000000000..50c3c0ccd
--- /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 3f6bf83a2..93d0c700b 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 b55297626..8bddc8e5a 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):
-- 
GitLab