From c7e7d9abb81ccccdc4e2dd0580a8194f51a848ae Mon Sep 17 00:00:00 2001
From: Tim Akinbo <41004+takinbo@users.noreply.github.com>
Date: Fri, 25 Oct 2024 18:26:37 +0100
Subject: [PATCH] hotfixes #7 (#1012)

* truly restrict forms to those available to the participant.

* fixed an issue where you would have a blank field and validation will fail.

* added the name field to the participant schema so it can be rendered in the selector.

* fixed bug where master checklists weren't also including section timestamp labels
---
 apollo/participants/api/schema.py     |   7 +-
 apollo/participants/api/views.py      |  12 +-
 apollo/pwa/static/js/serviceworker.js |   2 +-
 apollo/pwa/templates/pwa/index.html   |  26 +--
 apollo/submissions/services.py        | 262 +++++++++++---------------
 5 files changed, 140 insertions(+), 169 deletions(-)

diff --git a/apollo/participants/api/schema.py b/apollo/participants/api/schema.py
index d5dca9f6c..34d061243 100644
--- a/apollo/participants/api/schema.py
+++ b/apollo/participants/api/schema.py
@@ -6,12 +6,13 @@
 
 
 class ParticipantSchema(BaseModelSchema):
-    role = ma.fields.Method('get_role')
+    role = ma.fields.Method("get_role")
 
     class Meta:
+        """ParticipantSchema Meta."""
+
         model = Participant
-        fields = ('id', 'full_name', 'first_name', 'other_names', 'last_name',
-                  'participant_id', 'role')
+        fields = ("id", "name", "full_name", "first_name", "other_names", "last_name", "participant_id", "role")
 
     def get_role(self, obj):
         return obj.role.name
diff --git a/apollo/participants/api/views.py b/apollo/participants/api/views.py
index 17e1cf713..1228626d9 100644
--- a/apollo/participants/api/views.py
+++ b/apollo/participants/api/views.py
@@ -235,10 +235,14 @@ def _get_form_data(participant: Participant):
     )
 
     # get participant submissions
-    participant_submissions = Submission.query.join(
-        EventAlias,
-        and_(EventAlias.participant_set_id == participant.participant_set_id, Submission.event_id == EventAlias.id),
-    ).join(FormAlias, Submission.form_id == FormAlias.id)
+    participant_submissions = (
+        Submission.query.filter(Submission.participant_id == participant.id)
+        .join(
+            EventAlias,
+            and_(EventAlias.participant_set_id == participant.participant_set_id, Submission.event_id == EventAlias.id),
+        )
+        .join(FormAlias, Submission.form_id == FormAlias.id)
+    )
 
     # get checklist and survey forms based on the available submissions
     non_incident_forms = participant_submissions.with_entities(FormAlias).distinct(FormAlias.id)
diff --git a/apollo/pwa/static/js/serviceworker.js b/apollo/pwa/static/js/serviceworker.js
index cf33a03f9..4c29ce324 100644
--- a/apollo/pwa/static/js/serviceworker.js
+++ b/apollo/pwa/static/js/serviceworker.js
@@ -1,4 +1,4 @@
-const CACHE_NAME = 'apollo-cache-static-v11';
+const CACHE_NAME = 'apollo-cache-static-v12';
 
 const CACHED_URLS = [
   '/pwa/',
diff --git a/apollo/pwa/templates/pwa/index.html b/apollo/pwa/templates/pwa/index.html
index 1c48f1e6a..0e9aee5de 100644
--- a/apollo/pwa/templates/pwa/index.html
+++ b/apollo/pwa/templates/pwa/index.html
@@ -340,7 +340,7 @@ <h6 class="card-header" :class="getCompletionClass(index)"><a class="text-white"
       };
       if (a.form_type !== b.form_type)
         return FORM_TYPE_MAP[a.form_type] - FORM_TYPE_MAP[b.form_type];
-      
+
       if (a.name > b.name)
         return 1;
       else if (a.name < b.name)
@@ -381,7 +381,7 @@ <h6 class="card-header" :class="getCompletionClass(index)"><a class="text-white"
           acc += ((data[field.tag] !== undefined && data[field.tag] !== '') ? 1 : 0);
         return acc;
       }, 0);
-      
+
       return completion;
     };
 
@@ -394,7 +394,7 @@ <h6 class="card-header" :class="getCompletionClass(index)"><a class="text-white"
 
       if ((completion.total === completion.filled) && submission.passedQA)
         return true;
-      
+
       return false;
     };
 
@@ -479,7 +479,7 @@ <h6 class="card-header" :class="getCompletionClass(index)"><a class="text-white"
             status = '{{ _("Partial") }}';
           else
             status = '{{ _("Complete") }}';
-          
+
           return status;
         }
       },
@@ -531,7 +531,7 @@ <h6 class="card-header" :class="getCompletionClass(index)"><a class="text-white"
             return true;
           else if (this.submission.updated.getTime() > this.submission.posted.getTime())
             return true;
-          
+
           return false;
         },
         showQAStatus() {
@@ -745,7 +745,7 @@ <h6 class="card-header" :class="getCompletionClass(index)"><a class="text-white"
               });
             });
           }
-          
+
           return sub;
         },
         newSubmission(form) {
@@ -847,7 +847,7 @@ <h6 class="card-header" :class="getCompletionClass(index)"><a class="text-white"
               if (instance.data[field.tag] !== undefined || instance.data[field.tag] !== null) {
                 if ((fieldData < field.min) || (fieldData > field.max))
                   errorFields.push(field.tag);
-                else if (Number.parseInt(fieldData.toString()) !== fieldData)
+                else if (fieldData !== undefined && Number.parseInt(fieldData.toString()) !== fieldData)
                   errorFields.push(field.tag);
               }
             });
@@ -1048,10 +1048,10 @@ <h6 class="card-header" :class="getCompletionClass(index)"><a class="text-white"
           return instance.submissions.filter(submission => {
             if (submission.posted === null)
               return true;
-            
+
             if (submission.updated.getTime() > submission.posted.getTime())
               return true;
-            
+
             return false;
           }).length;
         },
@@ -1059,7 +1059,7 @@ <h6 class="card-header" :class="getCompletionClass(index)"><a class="text-white"
           let instance = this;
           if (instance.forms === null || instance.forms === [] || instance.submissions === null || instance.submissions === [])
             return {};
-          
+
           return instance.forms.reduce((acc, form) => {
             acc[form.id] = instance.submissionsSubset(form);
             return acc;
@@ -1172,7 +1172,7 @@ <h6 class="card-header" :class="getCompletionClass(index)"><a class="text-white"
             let formIds = new Set(forms.map(form => form.id));
             instance._loadCollectionFromDatabase(db.forms, dbForms => {
               unusedFormIds = dbForms.filter(form => !formIds.has(form.id)).map(form => form.id);
-              
+
               db.forms.bulkPut(forms);
               db.forms.bulkDelete(unusedFormIds);
 
@@ -1410,7 +1410,7 @@ <h6 class="card-header" :class="getCompletionClass(index)"><a class="text-white"
 
             if (instance.currentSubmission === submission)
               return false;
-            
+
             return !submission.passedQA;
           })
           .forEach(submission => {
@@ -1449,7 +1449,7 @@ <h6 class="card-header" :class="getCompletionClass(index)"><a class="text-white"
         globalStatus() {
           if (this.submissions.length === 0)
             return 0;
-          
+
           let unchangedSubmissions = this.submissions.filter(sub => sub.updated === null);
           if (unchangedSubmissions.length === this.submissions.length)
             return 0;
diff --git a/apollo/submissions/services.py b/apollo/submissions/services.py
index 1f8653237..732fd52a2 100644
--- a/apollo/submissions/services.py
+++ b/apollo/submissions/services.py
@@ -11,42 +11,41 @@
 from apollo.dal.service import Service
 from apollo.locations.models import LocationType, LocationTypePath
 from apollo.participants.models import Sample
-from apollo.submissions.models import (
-    Submission, SubmissionComment, SubmissionVersion)
+from apollo.submissions.models import Submission, SubmissionComment, SubmissionVersion
 from apollo.submissions.qa.query_builder import generate_qa_queries
 
 
 def export_field_value(form, submission, tag):
+    """Formatter for exporting field values."""
     field = form.get_field_by_tag(tag)
     data = submission.data.get(tag) if submission.data else None
 
-    if field['type'] == 'multiselect':
+    if field["type"] == "multiselect":
         if data:
             try:
-                return ','.join(sorted(str(i) for i in data))
+                return ",".join(sorted(str(i) for i in data))
             except TypeError:
                 return data
-    elif field['type'] == 'image':
+    elif field["type"] == "image":
         return data is not None
 
     return data
 
 
 def export_timestamp(ts_string: str) -> str:
+    """Formatter for exporting timestamp fields."""
     try:
         dt = isoparse(ts_string)
     except Exception:
-        return ''
-    
-    return dt.strftime('%Y-%m-%d %H:%M:%S')
+        return ""
+
+    return dt.strftime("%Y-%m-%d %H:%M:%S")
 
 
 class SubmissionService(Service):
     __model__ = Submission
 
-    def export_list(
-            self, query, include_qa=False, include_group_timestamps=False
-        ):
+    def export_list(self, query, include_qa=False, include_group_timestamps=False):
         if query.count() == 0:
             raise StopIteration
 
@@ -54,35 +53,25 @@ def export_list(
         event = submission.event
         form = submission.form
         extra_fields = event.location_set.extra_fields
-        location_types = LocationTypePath.query.filter_by(
-            location_set_id=event.location_set_id
-        ).join(
-            LocationType, LocationType.id == LocationTypePath.ancestor_id
-        ).with_entities(
-            LocationType
-        ).group_by(
-            LocationTypePath.ancestor_id,
-            LocationType.id
-        ).order_by(
-            sa.func.count(LocationTypePath.ancestor_id).desc(),
-            LocationType.name
-        ).all()
-        samples = Sample.query.filter_by(
-            participant_set_id=event.participant_set_id).all()
-        tags = form.unsorted_tags
+        location_types = (
+            LocationTypePath.query.filter_by(location_set_id=event.location_set_id)
+            .join(LocationType, LocationType.id == LocationTypePath.ancestor_id)
+            .with_entities(LocationType)
+            .group_by(LocationTypePath.ancestor_id, LocationType.id)
+            .order_by(sa.func.count(LocationTypePath.ancestor_id).desc(), LocationType.name)
+            .all()
+        )
+        samples = Sample.query.filter_by(participant_set_id=event.participant_set_id).all()
         form._populate_group_cache()
         form_groups = form._group_cache.keys()
-        group_tags = {
-            group: form.get_group_tags(group)
-            for group in form_groups
-        }
+        group_tags = {group: form.get_group_tags(group) for group in form_groups}
 
         extra_field_headers = [fi.label for fi in extra_fields]
 
         sample_headers = [s.name for s in samples]
 
-        if form.form_type == 'SURVEY':
-            dataset_headers = [_('Serial')]
+        if form.form_type == "SURVEY":
+            dataset_headers = [_("Serial")]
         else:
             dataset_headers = []
 
@@ -94,35 +83,32 @@ def export_list(
             )
         quality_checks = form.quality_checks if export_qa else []
 
-        dataset_headers.extend([
-            _('Participant ID'), _('Name'), _('DB Phone'), _('Recent Phone')
-        ] + [
-            loc_type.name for loc_type in location_types
-        ] + [
-            _('Location'), _('Location Code'), _('Latitude'), _('Longitude')
-        ] + extra_field_headers + [
-            _('Registered Voters')
-        ])
+        dataset_headers.extend(
+            [_("Participant ID"), _("Name"), _("DB Phone"), _("Recent Phone")]
+            + [loc_type.name for loc_type in location_types]
+            + [_("Location"), _("Location Code"), _("Latitude"), _("Longitude")]
+            + extra_field_headers
+            + [_("Registered Voters")]
+        )
 
         # add in group headers
         for group_name in form_groups:
-            if include_group_timestamps:
-                dataset_headers.append(_('%(group)s updated', group=group_name))
+            if include_group_timestamps or submission.submission_type != "O":
+                dataset_headers.append(_("%(group)s updated", group=group_name))
             dataset_headers.extend(group_tags[group_name])
-        
-        dataset_headers.append(_('Timestamp'))
 
-        if form.form_type == 'INCIDENT':
-            dataset_headers.extend([_('Status'), _('Description')])
+        dataset_headers.append(_("Timestamp"))
+
+        if form.form_type == "INCIDENT":
+            dataset_headers.extend([_("Status"), _("Description")])
         else:
             dataset_headers.extend(sample_headers)
-            if submission.submission_type == 'O':
-                dataset_headers.append(_('Comment'))
-                dataset_headers.append(_('Quarantine Status'))
+            if submission.submission_type == "O":
+                dataset_headers.append(_("Comment"))
+                dataset_headers.append(_("Quarantine Status"))
 
                 if export_qa:
-                    dataset_headers.extend(
-                        [qc['description'] for qc in quality_checks])
+                    dataset_headers.extend([qc["description"] for qc in quality_checks])
 
         output = StringIO()
         output.write(constants.BOM_UTF8_STR)
@@ -134,118 +120,98 @@ def export_list(
         for item in query:
             if export_qa:
                 row_dict = item._asdict()
-                submission = row_dict['Submission']
+                submission = row_dict["Submission"]
             else:
                 submission = item
                 row_dict = {}
             location_path = submission.location.make_path()
-            group_timestamps = (submission.extra_data or {}).get(
-                'group_timestamps', {})
-            if submission.submission_type == 'O':
+            group_timestamps = (submission.extra_data or {}).get("group_timestamps", {})
+            if submission.submission_type == "O":
                 if submission.location.extra_data:
-                    extra_data_columns = [
-                        submission.location.extra_data.get(ef.name)
-                        for ef in extra_fields
-                    ]
+                    extra_data_columns = [submission.location.extra_data.get(ef.name) for ef in extra_fields]
                 else:
-                    extra_data_columns = [''] * len(extra_fields)
-
-                record = [submission.serial_no] if form.form_type == 'SURVEY' else []   # noqa
-
-                record.extend([
-                    submission.participant.participant_id
-                    if submission.participant else '',
-                    submission.participant.name
-                    if submission.participant else '',
-                    submission.participant.primary_phone
-                    if submission.participant else '',
-                    submission.last_phone_number if submission.last_phone_number else '',   # noqa
-                ] + [
-                    location_path.get(loc_type.name, '')
-                    for loc_type in location_types
-                ] + [
-                    submission.location.name,
-                    submission.location.code,
-                    to_shape(submission.geom).y if hasattr(submission.geom, 'desc') else '',  # noqa
-                    to_shape(submission.geom).x if hasattr(submission.geom, 'desc') else ''  # noqa
-                ] + extra_data_columns + [
-                    submission.location.registered_voters
-                ])
+                    extra_data_columns = [""] * len(extra_fields)
+
+                record = [submission.serial_no] if form.form_type == "SURVEY" else []  # noqa
+
+                record.extend(
+                    [
+                        submission.participant.participant_id if submission.participant else "",
+                        submission.participant.name if submission.participant else "",
+                        submission.participant.primary_phone if submission.participant else "",
+                        submission.last_phone_number if submission.last_phone_number else "",  # noqa
+                    ]
+                    + [location_path.get(loc_type.name, "") for loc_type in location_types]
+                    + [
+                        submission.location.name,
+                        submission.location.code,
+                        to_shape(submission.geom).y if hasattr(submission.geom, "desc") else "",  # noqa
+                        to_shape(submission.geom).x if hasattr(submission.geom, "desc") else "",  # noqa
+                    ]
+                    + extra_data_columns
+                    + [submission.location.registered_voters]
+                )
 
                 for group_name in form_groups:
                     if include_group_timestamps:
-                        record.append(
-                            export_timestamp(group_timestamps.get(group_name, '')))
-                    record.extend([
-                        export_field_value(form, submission, tag)
-                        for tag in group_tags.get(group_name)
-                    ])
-
-                record += [
-                    submission.updated.strftime('%Y-%m-%d %H:%M:%S')
-                    if submission.updated else '',
-                    submission.incident_status.value
-                    if submission.incident_status else '',
-                    submission.incident_description
-                ] if form.form_type == 'INCIDENT' else ([
-                    submission.updated.strftime('%Y-%m-%d %H:%M:%S')
-                    if submission.updated else ''] + [
-                        1 if sample in submission.participant.samples else 0
-                        for sample in samples] + [
-                                submission.comments[0].comment.replace('\n', '') # noqa
-                                if submission.comments else '',
-                                submission.quarantine_status.value
-                                if submission.quarantine_status else '',
-                            ])
+                        record.append(export_timestamp(group_timestamps.get(group_name, "")))
+                    record.extend([export_field_value(form, submission, tag) for tag in group_tags.get(group_name)])
+
+                record += (
+                    [
+                        submission.updated.strftime("%Y-%m-%d %H:%M:%S") if submission.updated else "",
+                        submission.incident_status.value if submission.incident_status else "",
+                        submission.incident_description,
+                    ]
+                    if form.form_type == "INCIDENT"
+                    else (
+                        [submission.updated.strftime("%Y-%m-%d %H:%M:%S") if submission.updated else ""]
+                        + [1 if sample in submission.participant.samples else 0 for sample in samples]
+                        + [
+                            submission.comments[0].comment.replace("\n", "")  # noqa
+                            if submission.comments
+                            else "",
+                            submission.quarantine_status.value if submission.quarantine_status else "",
+                        ]
+                    )
+                )
 
                 if export_qa:
-                    record.extend(
-                        [row_dict[qc['name']] for qc in quality_checks])
+                    record.extend([row_dict[qc["name"]] for qc in quality_checks])
             else:
                 sib = submission.siblings[0]
                 if submission.location.extra_data:
-                    extra_data_columns = [
-                        submission.location.extra_data.get(ef.name)
-                        for ef in extra_fields
-                    ]
+                    extra_data_columns = [submission.location.extra_data.get(ef.name) for ef in extra_fields]
                 else:
-                    extra_data_columns = [''] * len(extra_fields)
-
-                record = [sib.serial_no] if form.form_type == 'SURVEY' else []
-
-                record.extend([
-                    sib.participant.participant_id
-                    if sib.participant else '',
-                    sib.participant.name
-                    if sib.participant else '',
-                    sib.participant.primary_phone
-                    if sib.participant else '',
-                    sib.last_phone_number if sib.last_phone_number else '',
-                ] + [
-                    location_path.get(loc_type.name, '')
-                    for loc_type in location_types
-                ] + [
-                    submission.location.name,
-                    submission.location.code,
-                    to_shape(sib.geom).y if hasattr(sib.geom, 'desc') else '', # noqa
-                    to_shape(sib.geom).x if hasattr(sib.geom, 'desc') else '', # noqa
-                ] + extra_data_columns + [
-                    submission.location.registered_voters
-                ])
+                    extra_data_columns = [""] * len(extra_fields)
+
+                record = [sib.serial_no] if form.form_type == "SURVEY" else []
+
+                record.extend(
+                    [
+                        sib.participant.participant_id if sib.participant else "",
+                        sib.participant.name if sib.participant else "",
+                        sib.participant.primary_phone if sib.participant else "",
+                        sib.last_phone_number if sib.last_phone_number else "",
+                    ]
+                    + [location_path.get(loc_type.name, "") for loc_type in location_types]
+                    + [
+                        submission.location.name,
+                        submission.location.code,
+                        to_shape(sib.geom).y if hasattr(sib.geom, "desc") else "",  # noqa
+                        to_shape(sib.geom).x if hasattr(sib.geom, "desc") else "",  # noqa
+                    ]
+                    + extra_data_columns
+                    + [submission.location.registered_voters]
+                )
 
                 for group_name in form_groups:
-                    record.append(
-                        export_timestamp(group_timestamps.get(group_name, '')))
-                    record.extend([
-                        export_field_value(form, submission, tag)
-                        for tag in group_tags.get(group_name)
-                    ])
-
-                record += [
-                    submission.updated.strftime('%Y-%m-%d %H:%M:%S')
-                    if submission.updated else ''] + [
-                        1 if sample in sib.participant.samples else 0
-                        for sample in samples]
+                    record.append(export_timestamp(group_timestamps.get(group_name, "")))
+                    record.extend([export_field_value(form, submission, tag) for tag in group_tags.get(group_name)])
+
+                record += [submission.updated.strftime("%Y-%m-%d %H:%M:%S") if submission.updated else ""] + [
+                    1 if sample in sib.participant.samples else 0 for sample in samples
+                ]
 
             output = StringIO()
             writer = csv.writer(output)