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)