diff --git a/backend/audit/cross_validation/naming.py b/backend/audit/cross_validation/naming.py index de31a73966..d3af9cdb3a 100644 --- a/backend/audit/cross_validation/naming.py +++ b/backend/audit/cross_validation/naming.py @@ -1,6 +1,8 @@ from types import new_class from typing import NamedTuple +from audit.models.submission_event import SubmissionEvent + # We need a canonical source of the different versions of each name. @@ -17,6 +19,7 @@ class SectionBabelFish(NamedTuple): snake_case: str # Mostly used for the field names in SingleAuditChecklist. url_tail: str | None # Hyphenated version of snake_case, mostly. workbook_number: int | None # Our upload ordering of workbooks. + submission_event: str # The event type we log to the SubmissionEvents table when this section is updated SECTION_NAMES = { @@ -29,6 +32,7 @@ class SectionBabelFish(NamedTuple): snake_case="additional_eins", url_tail="additional-eins", workbook_number=8, + submission_event=SubmissionEvent.EventType.ADDITIONAL_EINS_UPDATED, ), "additional_ueis": SectionBabelFish( all_caps="ADDITIONAL_UEIS", @@ -39,6 +43,7 @@ class SectionBabelFish(NamedTuple): snake_case="additional_ueis", url_tail="additional-ueis", workbook_number=6, + submission_event=SubmissionEvent.EventType.ADDITIONAL_UEIS_UPDATED, ), "audit_information": SectionBabelFish( all_caps="AUDIT_INFORMATION", @@ -49,6 +54,7 @@ class SectionBabelFish(NamedTuple): snake_case="audit_information", url_tail="audit-information", workbook_number=None, + submission_event=SubmissionEvent.EventType.AUDIT_INFORMATION_UPDATED, ), "corrective_action_plan": SectionBabelFish( all_caps="CORRECTIVE_ACTION_PLAN", @@ -59,6 +65,7 @@ class SectionBabelFish(NamedTuple): reverse_url="report_submission:CAP", url_tail="cap", workbook_number=5, + submission_event=SubmissionEvent.EventType.CORRECTIVE_ACTION_PLAN_UPDATED, ), "federal_awards": SectionBabelFish( all_caps="FEDERAL_AWARDS", @@ -69,6 +76,7 @@ class SectionBabelFish(NamedTuple): snake_case="federal_awards", url_tail="federal-awards", workbook_number=1, + submission_event=SubmissionEvent.EventType.FEDERAL_AWARDS_UPDATED, ), "findings_text": SectionBabelFish( all_caps="FINDINGS_TEXT", @@ -79,6 +87,7 @@ class SectionBabelFish(NamedTuple): snake_case="findings_text", url_tail="audit-findings-text", workbook_number=4, + submission_event=SubmissionEvent.EventType.FEDERAL_AWARDS_AUDIT_FINDINGS_TEXT_UPDATED, ), "findings_uniform_guidance": SectionBabelFish( all_caps="FINDINGS_UNIFORM_GUIDANCE", @@ -89,6 +98,7 @@ class SectionBabelFish(NamedTuple): snake_case="findings_uniform_guidance", url_tail="audit-findings", workbook_number=3, + submission_event=SubmissionEvent.EventType.FINDINGS_UNIFORM_GUIDANCE_UPDATED, ), "general_information": SectionBabelFish( all_caps="GENERAL_INFORMATION", @@ -99,6 +109,7 @@ class SectionBabelFish(NamedTuple): snake_case="general_information", url_tail="general-information", workbook_number=None, + submission_event=SubmissionEvent.EventType.GENERAL_INFORMATION_UPDATED, ), "notes_to_sefa": SectionBabelFish( all_caps="NOTES_TO_SEFA", @@ -109,6 +120,7 @@ class SectionBabelFish(NamedTuple): snake_case="notes_to_sefa", url_tail="notes-to-sefa", workbook_number=2, + submission_event=SubmissionEvent.EventType.NOTES_TO_SEFA_UPDATED, ), "single_audit_report": SectionBabelFish( all_caps="SINGLE_AUDIT_REPORT", @@ -119,6 +131,7 @@ class SectionBabelFish(NamedTuple): snake_case="single_audit_report", url_tail="upload-report", workbook_number=None, + submission_event=SubmissionEvent.EventType.AUDIT_REPORT_PDF_UPDATED, ), "secondary_auditors": SectionBabelFish( all_caps="SECONDARY_AUDITORS", @@ -129,6 +142,7 @@ class SectionBabelFish(NamedTuple): snake_case="secondary_auditors", url_tail="secondary-auditors", workbook_number=7, + submission_event=SubmissionEvent.EventType.SECONDARY_AUDITORS_UPDATED, ), "tribal_data_consent": SectionBabelFish( all_caps="TRIBAL_DATA_CONSENT", @@ -139,6 +153,7 @@ class SectionBabelFish(NamedTuple): snake_case="tribal_data_consent", url_tail=None, workbook_number=None, + submission_event=SubmissionEvent.EventType.TRIBAL_CONSENT_UPDATED, ), } diff --git a/backend/audit/cross_validation/submission_progress_check.py b/backend/audit/cross_validation/submission_progress_check.py index c61f9be124..67a7792374 100644 --- a/backend/audit/cross_validation/submission_progress_check.py +++ b/backend/audit/cross_validation/submission_progress_check.py @@ -1,4 +1,5 @@ from audit.cross_validation.naming import NC, find_section_by_name +from audit.models.submission_event import SubmissionEvent from audit.validators import validate_general_information_complete_json from django.core.exceptions import ValidationError @@ -44,7 +45,7 @@ def submission_progress_check(sac, sar=None, crossval=True): result = {k: None for k in sac["sf_sac_sections"]} for key in sac["sf_sac_sections"]: - result = result | progress_check(sac["sf_sac_sections"], key) + result = result | progress_check(sac, sac["sf_sac_sections"], key) incomplete_sections = [] for k in result: @@ -66,7 +67,7 @@ def submission_progress_check(sac, sar=None, crossval=True): ] -def progress_check(sections, key): +def progress_check(sac, sections, key): """ Given the content of sf_sac_sections from sac_validation_shape (plus a single_audit_report key) and a key, determine whether that key is required, and @@ -111,7 +112,7 @@ def get_num_findings(award): # The General Information has its own condition, as it can be partially completed. if key == "general_information": - return general_information_progress_check(progress, general_info) + return general_information_progress_check(progress, general_info, sac) # If it's not required, it's inactive: if not conditions[key]: @@ -119,12 +120,37 @@ def get_num_findings(award): # If it is required, it should be present if sections.get(key): - return {key: progress | {"display": "complete", "completed": True}} + completed_by, completed_date = section_completed_metadata(sac, key) + + return { + key: progress + | { + "display": "complete", + "completed": True, + "completed_by": completed_by, + "completed_date": completed_date, + } + } return {key: progress | {"display": "incomplete", "completed": False}} -def general_information_progress_check(progress, general_info): +def section_completed_metadata(sac, section_key): + try: + section = find_section_by_name(section_key) + event_type = section.submission_event + + report_id = sac["sf_sac_meta"]["report_id"] + event = SubmissionEvent.objects.filter( + sac__report_id=report_id, event=event_type + ).latest("timestamp") + + return event.user.email, event.timestamp + except SubmissionEvent.DoesNotExist: + return None, None + + +def general_information_progress_check(progress, general_info, sac): """ Given a base "progress" dictionary and the general_info object from a submission, run validations to determine its completeness. Then, return a dictionary with @@ -138,8 +164,18 @@ def general_information_progress_check(progress, general_info): is_general_info_complete = False if is_general_info_complete: + completed_by, completed_date = section_completed_metadata( + sac, "general_information" + ) + return { - "general_information": progress | {"display": "complete", "completed": True} + "general_information": progress + | { + "display": "complete", + "completed": True, + "completed_by": completed_by, + "completed_date": completed_date, + } } return { "general_information": progress | {"display": "incomplete", "completed": False} diff --git a/backend/audit/intake_to_dissemination.py b/backend/audit/intake_to_dissemination.py index a1fc3e490d..ac0290e585 100644 --- a/backend/audit/intake_to_dissemination.py +++ b/backend/audit/intake_to_dissemination.py @@ -25,12 +25,16 @@ def omit(remove, d) -> dict: class IntakeToDissemination(object): - def __init__(self, sac) -> None: + DISSEMINATION = "dissemination" + PRE_CERTIFICATION_REVIEW = "pre_certification_review" + + def __init__(self, sac, mode=DISSEMINATION) -> None: self.single_audit_checklist = sac self.report_id = sac.report_id audit_date = sac.general_information["auditee_fiscal_period_end"] self.audit_year = int(audit_date.split("-")[0]) self.loaded_objects: dict[str, list] = {} + self.mode = mode def load_all(self): load_methods = { @@ -280,18 +284,26 @@ def load_general(self): dates_by_status = self._get_dates_from_sac() status = self.single_audit_checklist.STATUS ready_for_certification_date = dates_by_status[status.READY_FOR_CERTIFICATION] - auditor_certified_date = dates_by_status[status.AUDITOR_CERTIFIED] - auditee_certified_date = dates_by_status[status.AUDITEE_CERTIFIED] - submitted_date = self._convert_utc_to_american_samoa_zone( - dates_by_status[status.SUBMITTED] - ) - fac_accepted_date = submitted_date - auditee_certify_name = auditee_certification["auditee_signature"][ - "auditee_name" - ] - auditee_certify_title = auditee_certification["auditee_signature"][ - "auditee_title" - ] + if self.mode == IntakeToDissemination.DISSEMINATION: + submitted_date = self._convert_utc_to_american_samoa_zone( + dates_by_status[status.SUBMITTED] + ) + fac_accepted_date = submitted_date + auditee_certify_name = auditee_certification["auditee_signature"][ + "auditee_name" + ] + auditee_certify_title = auditee_certification["auditee_signature"][ + "auditee_title" + ] + auditor_certified_date = dates_by_status[status.AUDITOR_CERTIFIED] + auditee_certified_date = dates_by_status[status.AUDITEE_CERTIFIED] + elif self.mode == IntakeToDissemination.PRE_CERTIFICATION_REVIEW: + submitted_date = None + fac_accepted_date = submitted_date + auditee_certify_name = None + auditee_certify_title = None + auditor_certified_date = None + auditee_certified_date = None total_amount_expended = self.single_audit_checklist.federal_awards[ "FederalAwards" diff --git a/backend/audit/templates/audit/auditee-certification-step-1.html b/backend/audit/templates/audit/auditee-certification-step-1.html index 462eee45c6..e5fc5e6cf8 100644 --- a/backend/audit/templates/audit/auditee-certification-step-1.html +++ b/backend/audit/templates/audit/auditee-certification-step-1.html @@ -1,14 +1,43 @@ {% extends "base.html" %} {% load static %} +{% load sprite_helper %} {% block content %}