diff --git a/.github/workflows/regression-tests.yml b/.github/workflows/regression-tests.yml index 9e8b3c4acd..8d4360344c 100644 --- a/.github/workflows/regression-tests.yml +++ b/.github/workflows/regression-tests.yml @@ -31,6 +31,9 @@ jobs: CYPRESS_LOGIN_TEST_EMAIL: ${{ secrets.CYPRESS_LOGIN_TEST_EMAIL }} CYPRESS_LOGIN_TEST_PASSWORD: ${{ secrets.CYPRESS_LOGIN_TEST_PASSWORD }} CYPRESS_LOGIN_TEST_OTP_SECRET: ${{ secrets.CYPRESS_LOGIN_TEST_OTP_SECRET }} + CYPRESS_LOGIN_TEST_EMAIL_AUDITEE: ${{ secrets.CYPRESS_LOGIN_TEST_EMAIL_AUDITEE }} + CYPRESS_LOGIN_TEST_PASSWORD_AUDITEE: ${{ secrets.CYPRESS_LOGIN_TEST_PASSWORD_AUDITEE }} + CYPRESS_LOGIN_TEST_OTP_SECRET_AUDITEE: ${{ secrets.CYPRESS_LOGIN_TEST_OTP_SECRET_AUDITEE }} DISABLE_AUTH: False # useful for debugging but otherwise complete overwhelm # DEBUG: cypress:* diff --git a/backend/audit/cross_validation/naming.py b/backend/audit/cross_validation/naming.py index 741629ccde..ed8885078b 100644 --- a/backend/audit/cross_validation/naming.py +++ b/backend/audit/cross_validation/naming.py @@ -33,7 +33,7 @@ class SectionBabelFish(NamedTuple): workbook_number: int | None # Our upload ordering of workbooks. -section_names = { +SECTION_NAMES = { "additional_eins": SectionBabelFish( all_caps="ADDITIONAL_EINS", camel_case="AdditionalEINs", @@ -89,9 +89,9 @@ class SectionBabelFish(NamedTuple): camel_case="FindingsText", friendly="Federal Awards Audit Findings Text", friendly_title="Federal Awards Audit Findings Text", - reverse_url="report_submission:audit-findings", + reverse_url="report_submission:audit-findings-text", snake_case="findings_text", - url_tail="audit-findings", + url_tail="audit-findings-text", workbook_number=4, ), "findings_uniform_guidance": SectionBabelFish( @@ -99,7 +99,7 @@ class SectionBabelFish(NamedTuple): camel_case="FindingsUniformGuidance", friendly="Findings Uniform Guidance", friendly_title="Federal Awards Audit Findings", - reverse_url="report_submission:audit_findings", + reverse_url="report_submission:audit-findings", snake_case="findings_uniform_guidance", url_tail="audit-findings", workbook_number=3, @@ -118,12 +118,32 @@ class SectionBabelFish(NamedTuple): all_caps="NOTES_TO_SEFA", camel_case="NotesToSefa", friendly="Notes to SEFA", - friendly_title="General Information form", - reverse_url="report_submission:general_information", - snake_case="general_information", - url_tail="general-information", + friendly_title="Notes to SEFA", + reverse_url="report_submission:notes-to-sefa", + snake_case="notes_to_sefa", + url_tail="notes-to-sefa", + workbook_number=2, + ), + "single_audit_report": SectionBabelFish( + all_caps="SINGLE_AUDIT_REPORT", + camel_case="SingleAuditReport", + friendly="Single Audit Report", + friendly_title="Audit report PDF", + reverse_url="audit:UploadReport", + snake_case="single_audit_report", + url_tail="upload-report", workbook_number=None, ), + "secondary_auditors": SectionBabelFish( + all_caps="SECONDARY_AUDITORS", + camel_case="SecondaryAuditors", + friendly="Secondary Auditors", + friendly_title="Secondary Auditors", + reverse_url="report_submission:secondary-auditors", + snake_case="secondary_auditors", + url_tail="secondary-auditors", + workbook_number=7, + ), "tribal_data_consent": SectionBabelFish( all_caps="TRIBAL_DATA_CONSENT", camel_case="TribalDataConsent", @@ -141,11 +161,23 @@ def find_section_by_name(name): """ Find the answers, first trying snake_case and then all the other versions. """ - if name in section_names: - return section_names[name] + if name in SECTION_NAMES: + return SECTION_NAMES[name] - for guide in section_names.values(): + for guide in SECTION_NAMES.values(): if name in guide: return guide return None + + +def camel_to_snake(camel_case_section_name): + """Helper function for finding section names.""" + guide = find_section_by_name(camel_case_section_name) + return guide.snake_case if guide else None + + +def snake_to_camel(snake_case_section_name): + """Helper function for finding section names.""" + guide = find_section_by_name(snake_case_section_name) + return guide.camel_case if guide else None diff --git a/backend/audit/cross_validation/sac_validation_shape.py b/backend/audit/cross_validation/sac_validation_shape.py index 27389bd1cd..58ebe36804 100644 --- a/backend/audit/cross_validation/sac_validation_shape.py +++ b/backend/audit/cross_validation/sac_validation_shape.py @@ -1,28 +1,21 @@ -camel_to_snake = { - "AdditionalUEIs": "additional_ueis", - "SecondaryAuditors": "secondary_auditors", - "AuditInformation": "audit_information", - "CorrectiveActionPlan": "corrective_action_plan", - "FederalAwards": "federal_awards", - "FindingsText": "findings_text", - "FindingsUniformGuidance": "findings_uniform_guidance", - "GeneralInformation": "general_information", - "NotesToSefa": "notes_to_sefa", - "TribalDataConsent": "tribal_data_consent", -} -snake_to_camel = {v: k for k, v in camel_to_snake.items()} +from audit.cross_validation.naming import ( + SECTION_NAMES, + camel_to_snake, + snake_to_camel, +) + at_root_sections = ("audit_information", "general_information") def get_shaped_section(sac, section_name): """Extract either None or the appropriate dict from the section.""" - true_name = camel_to_snake.get(section_name, section_name) + true_name = camel_to_snake(section_name) or section_name section = getattr(sac, true_name, None) if true_name in at_root_sections: return section if section: - return section.get(snake_to_camel.get(true_name), {}) + return section.get(snake_to_camel(true_name), {}) return None @@ -73,9 +66,7 @@ def sac_validation_shape(sac): """ shape = { - "sf_sac_sections": { - k: get_shaped_section(sac, k) for k in camel_to_snake.values() - }, + "sf_sac_sections": {k: get_shaped_section(sac, k) for k in SECTION_NAMES}, "sf_sac_meta": { "submitted_by": sac.submitted_by, "date_created": sac.date_created, diff --git a/backend/audit/cross_validation/submission_progress_check.py b/backend/audit/cross_validation/submission_progress_check.py index 5abdf738b0..2f7a161687 100644 --- a/backend/audit/cross_validation/submission_progress_check.py +++ b/backend/audit/cross_validation/submission_progress_check.py @@ -1,3 +1,6 @@ +from audit.cross_validation.naming import find_section_by_name + + def submission_progress_check(sac, sar=None, crossval=True): """ Because this function was initially created in a view and not as a @@ -8,8 +11,8 @@ def submission_progress_check(sac, sar=None, crossval=True): crossval defaults to True because we don't want to have to change all the calls to the validation functions to include this argument. - Given a SingleAuditChecklist instance and a SingleAuditReportFile instance, - return information about submission progress. + Given the output of sac_validation_shape on a SingleAuditChecklist instance, and + a SingleAuditReportFile instance, return information about submission progress. Returns this shape: @@ -25,63 +28,35 @@ def submission_progress_check(sac, sar=None, crossval=True): single_audit_report, and [progress_dict] is: { + "section_sname": [snake_case name of section], "display": "hidden"/"incomplete"/"complete", "completed": [bool], "completed_by": [email], "completed_date": [date], } """ - sections = sac["sf_sac_sections"] - # TODO: remove these once tribal data consent are implemented - del sections["tribal_data_consent"] - result = {k: None for k in sections} # type: ignore - progress = { - "display": None, - "completed": None, - "completed_by": None, - "completed_date": None, - } - - cond_keys = _conditional_keys_progress_check(sections) - for ckey, cvalue in cond_keys.items(): - result[ckey] = progress | cvalue - - mandatory_keys = _mandatory_keys_progress_check(sections, cond_keys) - for mkey, mvalue in mandatory_keys.items(): - result[mkey] = progress | mvalue - - sar_progress = { - "display": "complete" if bool(sar) else "incomplete", - "completed": bool(sar), - } - - result["single_audit_report"] = progress | sar_progress # type: ignore - - complete = False + # TODO: remove these once tribal data consent are implemented: + del sac["sf_sac_sections"]["tribal_data_consent"] - def cond_pass(cond_key): - passing = ("hidden", "complete") - return result.get(cond_key, {}).get("display") in passing + # Add the status of the SAR into the list of sections: + sac["sf_sac_sections"]["single_audit_report"] = bool(sar) - error_keys = ( - list(mandatory_keys.keys()) + list(cond_keys.keys()) + ["single_audit_report"] - ) + result = {k: None for k in sac["sf_sac_sections"]} - # Need this to return useful errors in cross-validation: - incomplete_sections = [ - k for k in error_keys if result[k].get("display") == "incomplete" - ] + for key in sac["sf_sac_sections"]: + result = result | progress_check(sac["sf_sac_sections"], key) - if all(bool(sections[k]) for k in mandatory_keys): - if all(cond_pass(j) for j in cond_keys): - complete = True + incomplete_sections = [] + for k in result: + if result[k].get("display") == "incomplete": + incomplete_sections.append(find_section_by_name(k).friendly) - result["complete"] = complete # type: ignore + result["complete"] = len(incomplete_sections) == 0 if not crossval: - return result # return the cross-validation shape + return result # return the submission progress shape. - if complete: + if result["complete"]: return [] return [ @@ -91,41 +66,51 @@ def cond_pass(cond_key): ] -def _conditional_keys_progress_check(sections): +def progress_check(sections, key): """ - Support function for submission_progress_check; handles the conditional sections. + 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 + return a dictionary containing that key with its progress as the value. """ - general_info = sections.get("general_information") or {} - conditional_keys = { - "additional_ueis": general_info.get("multiple_ueis_covered"), - # Update once we have the question in. This may be handled in the gen info form rather than as a workbook. - "additional_eins": general_info.get("multiple_eins_covered"), - "secondary_auditors": general_info.get( - "secondary_auditors_exist" - ), # update this once we have the question in. + + def get_num_findings(award): + if program := award.get("program"): + if findings := program.get("number_of_audit_findings", 0): + return int(findings) + return 0 + + progress = { + "display": None, + "completed": None, + "completed_by": None, + "completed_date": None, + "section_name": key, } - output = {} - for key, value in conditional_keys.items(): - current = "incomplete" - if not value: - current = "hidden" - elif sections.get(key): - current = "complete" - info = {"display": current, "completed": current == "complete"} - output[key] = info - return output - - -def _mandatory_keys_progress_check(sections, conditional_keys): - """ - Support function for submission_progress_check; handles the mandatory sections. - """ - other_keys = [k for k in sections if k not in conditional_keys] - output = {} - for k in other_keys: - if bool(sections[k]): - info = {"display": "complete", "completed": True} - else: - info = {"display": "incomplete", "completed": False} - output[k] = info - return output + awards = {} + if sections["federal_awards"]: + awards = sections.get("federal_awards", {}).get("federal_awards", []) + general_info = sections.get("general_information", {}) or {} + num_findings = sum(get_num_findings(award) for award in awards) + conditions = { + "general_information": True, + "audit_information": True, + "federal_awards": True, + "notes_to_sefa": True, + "findings_uniform_guidance": num_findings > 0, + "findings_text": num_findings > 0, + "corrective_action_plan": num_findings > 0, + "additional_ueis": bool(general_info.get("multiple_ueis_covered")), + "additional_eins": bool(general_info.get("multiple_eins_covered")), + "secondary_auditors": bool(general_info.get("secondary_auditors_exist")), + "single_audit_report": True, + } + + # If it's not required, it's inactive: + if not conditions[key]: + return {key: progress | {"display": "inactive"}} + + # If it is required, it should be present + if sections.get(key): + return {key: progress | {"display": "complete", "completed": True}} + + return {key: progress | {"display": "incomplete", "completed": False}} diff --git a/backend/audit/templates/audit/audit-info-form.html b/backend/audit/templates/audit/audit-info-form.html index c837d6db81..cde3614af5 100644 --- a/backend/audit/templates/audit/audit-info-form.html +++ b/backend/audit/templates/audit/audit-info-form.html @@ -234,7 +234,7 @@

Federal programs

- What is the dollar threshold to distinguish Type A ($750,000 or greater) and Type B programs (less than $750,000)? (Uniform Guidance § 200.518(b)(1) * + What is the dollar threshold to distinguish Type A and Type B programs? (Uniform Guidance § 200.518(b)(1)) *
diff --git a/backend/audit/templates/audit/submission_checklist/icon-list-icon.html b/backend/audit/templates/audit/submission_checklist/icon-list-icon.html index 565e5037bc..355576db44 100644 --- a/backend/audit/templates/audit/submission_checklist/icon-list-icon.html +++ b/backend/audit/templates/audit/submission_checklist/icon-list-icon.html @@ -1,21 +1,30 @@ {% comment %}
for icon list icon. - If completed is true, use a success-darker (dark green) check. Else, an empty space. + If completed, use a success-darker (dark green) check. + If incomplete, an empty checkbox. + Else if inactive, an empty space. Variables: completed (bool) + display (str) {% endcomment %} {% load sprite_helper %} {% if completed %} -
- -
+
+ +
+{% elif display == 'incomplete' %} +
+ +
{% else %} -
- -
+
+ +
{% endif %} diff --git a/backend/audit/templates/audit/submission_checklist/submission-checklist.html b/backend/audit/templates/audit/submission_checklist/submission-checklist.html index 83f52ce8bb..1eeea2ef12 100644 --- a/backend/audit/templates/audit/submission_checklist/submission-checklist.html +++ b/backend/audit/templates/audit/submission_checklist/submission-checklist.html @@ -1,6 +1,7 @@ {% extends "base.html" %} {% load static %} {% load sprite_helper %} +{% load submission_progress_tags %} {% block content %}
@@ -9,325 +10,20 @@
    - - {% comment %} Single audit submission {% endcomment %} -
  • - {% include './icon-list-icon.html' with completed=single_audit_checklist.created %} -
    -

    General information form (Complete)

    -

    - Begin your single audit submission by entering basic information about the auditee organization. -

    - {% include './created-by.html' with time=single_audit_checklist.created_date name=single_audit_checklist.created_by %} -

    - Edit the audit information -

    -
    -
  • - - {% comment %} Audit Information Form {% endcomment %} -
  • - {% if audit_information.completed %} - {% include './icon-list-icon.html' with completed=audit_information.completed %} -
    -

    Audit Information Form (Complete)

    -

    - This form is completed by both the auditee and auditor. Auditees complete this section using the Summary Schedule of Prior Audit Findings and Auditors complete this section using the information in the financial statement audit. -

    - {% include './created-by.html' with time=audit_information.completed_date name=audit_information.created_by %} -

    - Edit the Audit Information Form -

    -
    - {% else %} - {% include './icon-list-icon.html' with completed=audit_information.completed %} -
    -

    - Audit Information Form -

    -

    - This form is completed by both the auditee and auditor. Auditees complete this section using the Summary Schedule of Prior Audit Findings and Auditors complete this section using the information in the financial statement audit. -

    -
    - {% endif %} -
  • - - {% comment %} Audit report PDF {% endcomment %} -
  • - {% if single_audit_report.completed %} - {% include './icon-list-icon.html' with completed=single_audit_report.completed %} -
    -

    Audit report PDF (Complete)

    -

    - Upload the single audit report package, or audit report. This should be a single PDF that is unlocked and machine-readable. -

    - {% include './created-by.html' with time=single_audit_report.completed_date name=single_audit_report.created_by %} -

    - Re-upload the Audit report PDF -

    -
    - {% else %} - {% include './icon-list-icon.html' with completed=single_audit_report.completed %} -
    -

    - Audit report PDF -

    -

    - Upload the single audit report package, or audit report. This should be a single PDF that is unlocked and machine-readable. -

    -
    - {% endif %} -
  • - - {% comment %} Workbook 1: Federal Awards {% endcomment %} -
  • - {% if federal_awards.completed %} - {% include './icon-list-icon.html' with completed=federal_awards.completed %} -
    -

    Workbook 1: Federal Awards (Complete)

    -

    - For each federal award received, you'll need the financial and agency details. This is also where you list the number of audit findings. -

    - {% include './created-by.html' with time=federal_awards.completed_date name=federal_awards.created_by %} -

    - Re-upload Workbook 1: Federal Awards -

    -
    - {% else %} - {% include './icon-list-icon.html' with completed=federal_awards.completed %} -
    -

    - Upload Workbook 1: Federal Awards -

    -

    - For each federal award received, you'll need the financial and agency details. This is also where you list the number of audit findings. -

    -
    - {% endif %} -
  • - - {% comment %} Workbook 2: Notes to SEFA {% endcomment %} -
  • - {% if notes_to_sefa.completed %} - {% include './icon-list-icon.html' with completed=notes_to_sefa.completed %} -
    -

    Workbook 2: Notes to SEFA (Complete)

    -

    - This workbook covers notes on the Schedule of Expenditures of Federal Awards (SEFA). Enter the information of each Federal awards program from which the auditee received funds, even if they don't have audit findings. -

    - {% include './created-by.html' with time=notes_to_sefa.completed_date name=notes_to_sefa.created_by %} -

    - Re-upload Workbook 2: Notes to SEFA -

    -
    - {% else %} - {% include './icon-list-icon.html' with completed=notes_to_sefa.completed %} -
    -

    - Upload Workbook 2: Notes to SEFA -

    -

    - This workbook covers notes on the Schedule of Expenditures of Federal Awards (SEFA). Enter the information of each Federal awards program from which the auditee received funds, even if they don't have audit findings. -

    -
    - {% endif %} -
  • - - {% comment %} Workbook 3: Federal Awards Audit Findings {% endcomment %} -
  • - {% if findings_uniform_guidance.completed %} - {% include './icon-list-icon.html' with completed=findings_uniform_guidance.completed %} -
    -

    Workbook 3: Federal Awards Audit Findings (Complete)

    -

    - Complete this section using the Summary Schedule of Prior Audit Findings and the information in the financial statement audit. -

    - {% include './created-by.html' with time=findings_uniform_guidance.completed_date name=findings_uniform_guidance.created_by %} -

    - Re-upload Workbook 3: Federal Awards Audit Findings -

    -
    - {% else %} - {% include './icon-list-icon.html' with completed=findings_uniform_guidance.completed %} -
    -

    - Upload Workbook 3: Federal Awards Audit Findings -

    -

    - Complete this section using the Summary Schedule of Prior Audit Findings and the information in the financial statement audit. -

    -
    - {% endif %} -
  • - - {% comment %} Workbook 4: Federal Awards Audit Findings Text {% endcomment %} -
  • - {% if findings_text.completed %} - {% include './icon-list-icon.html' with completed=findings_text.completed %} -
    -

    Workbook 4: Federal Awards Audit Findings Text (Complete)

    -

    - Enter the full text of the audit finding, listing each finding only once, even if they relate to more than one program. Include the audit finding reference number for each. -

    - {% include './created-by.html' with time=findings_text.completed_date name=findings_text.created_by %} -

    - Re-upload Workbook 4: Federal Awards Audit Findings Text -

    -
    - {% else %} - {% include './icon-list-icon.html' with completed=findings_text.completed %} -
    -

    - Upload Workbook 4: Federal Awards Audit Findings Text -

    -

    - Enter the full text of the audit finding, listing each finding only once, even if they relate to more than one program. Include the audit finding reference number for each. -

    -
    - {% endif %} -
  • - - {% comment %} Workbook 5: Corrective Action Plan (CAP) {% endcomment %} -
  • - {% if corrective_action_plan.completed %} - {% include './icon-list-icon.html' with completed=corrective_action_plan.completed %} -
    -

    Workbook 5: Corrective Action Plan (CAP) (Complete)

    -

    - This information should match the data you entered in the Findings Text workbook. You only need to enter plans for findings once if they relate to more than one program. -

    - {% include './created-by.html' with time=corrective_action_plan.completed_date name=corrective_action_plan.created_by %} -

    - Re-upload Workbook 5: Corrective Action Plan (CAP) -

    -
    - {% else %} - {% include './icon-list-icon.html' with completed=corrective_action_plan.completed %} -
    -

    - Upload Workbook 5: Corrective Action Plan (CAP) -

    -

    - This information should match the data you entered in the Findings Text workbook. You only need to enter plans for findings once if they relate to more than one program. -

    -
    - {% endif %} -
  • - - {% comment %} Workbook 6: Additional UEIs (optional) {% endcomment %} - {% if additional_ueis.display != "hidden" %} -
  • - {% if additional_ueis.completed %} - {% include './icon-list-icon.html' with completed=additional_ueis.completed %} -
    -

    Workbook 6: Additional UEIs (Complete)

    -

    - This workbook is only necessary if multiple the audit report covers UEIs. It is completed by the auditee. List the additional UEIs covered by the audit, excluding the primary UEI. -

    - {% include './created-by.html' with time=additional_ueis.completed_date name=additional_ueis.created_by %} -

    - Re-upload Workbook 6: Additional UEIs -

    -
    - {% else %} - {% include './icon-list-icon.html' with completed=additional_ueis.completed %} -
    -

    - Upload Workbook 6: Additional UEIs (optional) -

    -

    - This workbook is only necessary if multiple the audit report covers UEIs. It is completed by the auditee. List the additional UEIs covered by the audit, excluding the primary UEI. -

    -
    - {% endif %} -
  • - {% endif %} - - {% comment %} Upload Workbook 7: Secondary Auditors (optional) {% endcomment %} - {% if secondary_auditors.display != "hidden" %} -
  • - {% if secondary_auditors.completed %} - {% include './icon-list-icon.html' with completed=secondary_auditors.completed %} -
    -

    Workbook 7: Secondary Auditors (Complete)

    -

    - This workbook is only necessary if multiple auditors did the audit work. In these cases, the secondary auditors should enter their information in this workbook. -

    - {% include './created-by.html' with time=secondary_auditors.completed_date name=secondary_auditors.created_by %} -

    - Re-upload Workbook 7: Secondary Auditors -

    -
    - {% else %} - {% include './icon-list-icon.html' with completed=secondary_auditors.completed %} -
    -

    - Upload Workbook 7: Secondary Auditors (optional) -

    -

    - This workbook is only necessary if multiple auditors did the audit work. In these cases, the secondary auditors should enter their information in this workbook. -

    -
    - {% endif %} -
  • - {% endif %} - {% comment %} Upload Workbook 8: Secondary Auditors (optional) {% endcomment %} - {% if additional_eins.display != "hidden" %} -
  • - {% if additional_eins.completed %} - {% include './icon-list-icon.html' with completed=additional_eins.completed %} -
    -

    Workbook 8: Additional EINs (Complete)

    -

    - This workbook is only necessary if the audit report covers multiple EINs. List the additional EINs covered by the audit, excluding the primary EIN. -

    - {% include './created-by.html' with time=additional_eins.completed_date name=additional_eins.created_by %} -

    - Re-upload Workbook 8: Additional EINs -

    -
    - {% else %} - {% include './icon-list-icon.html' with completed=additional_eins.completed %} -
    -

    - Upload Workbook 8: Additional EINs (optional) -

    -

    - This workbook is only necessary if the audit report covers multiple EINs. List the additional EINs covered by the audit, excluding the primary EIN. -

    -
    - {% endif %} -
  • - {% endif %} + {% section_block report_id general_information %} + {% section_block report_id audit_information %} + {% section_block report_id single_audit_report %} + {% section_block report_id federal_awards %} + {% section_block report_id notes_to_sefa %} + {% section_block report_id findings_uniform_guidance %} + {% section_block report_id findings_text %} + {% section_block report_id corrective_action_plan %} + {% section_block report_id additional_ueis %} + {% section_block report_id secondary_auditors %} + {% section_block report_id additional_eins %} - {% comment %} Key information (certification steps) {% endcomment %} + {% comment %} Key information (certification steps) {% endcomment %}
    + + {% include './audit/submission_checklist/icon-list-icon.html' with completed=completed display=display %} + {% if completed %} +
    +

    {{ title }}

    +

    + {{ text }} + {% if text_nb %} + {{ text_nb }} + {% endif %} +

    + + {% comment %} +

    for created by time and name. + If no name is available, uses email. + + Examples: + Completed (formatted time) by (email). + Completed MMM DD, YYYY at 12:00 AM UST by example@example.gov. + + Variables: + completed_date (string) + completed_by (string) + {% endcomment %} +

    + {% if completed_by and completed_date %} + Completed {{ completed_date|date:"N j, Y g:i A T" }} by {{ completed_by }}. + {% elif completed_date %} + Completed {{ completed_date|date:"N j, Y g:i A T" }}. + {% endif %} +

    +

    + {{ edit_text }} +

    +
    + {% elif display == 'inactive' %} +
    +

    {{ title }}

    +

    + {{ text }} + {% if text_nb %} + {{ text_nb }} + {% endif %} +

    +
    + {% else %} {% comment %} This is the incomplete state {% endcomment %} +
    +

    + {{ title }} +

    +

    + {{ text }} + {% if text_nb %} + {{ text_nb }} + {% endif %} +

    +
    + {% endif %} + diff --git a/backend/audit/templatetags/submission_progress_tags.py b/backend/audit/templatetags/submission_progress_tags.py new file mode 100644 index 0000000000..b80719e979 --- /dev/null +++ b/backend/audit/templatetags/submission_progress_tags.py @@ -0,0 +1,39 @@ +""" +Custom tag to support the logic around sections on the Submission progress page. +""" +from django import template + +register = template.Library() + + +@register.inclusion_tag("section_block.html") +def section_block(report_id, section_info): + """ + Returns the context dict that the above template will use. + Most of the work for this is done in + cross_validation.submission_progress_check.progress_check, because that's a + little more centralized and troubleshooting is a little easier if the work is done before + template rendering rather than during. + """ + additional_info = { + "report_id": report_id, + } + title = section_info["friendly_title"] + if workbook_number := section_info["workbook_number"]: + title = f"Upload Workbook {workbook_number}: {title}" + if section_info.get("completed") is True: + title = f"{title} (Complete)" + additional_info["title"] = title + additional_info["ctx"] = section_info + return section_info | additional_info + + +""" +python manage.py test audit.test_submission_progress_view +python manage.py shell +from audit.templatetags.submission_progress_tags import section_block as sb +sb('whatever', {}, "general_information") +from audit.cross_validation.sac_validation_shape import get_shaped_section +from audit.models import SingleAuditChecklist +sac = SingleAuditChecklist.objects.get(id=2) +""" diff --git a/backend/audit/test_submission_progress_view.py b/backend/audit/test_submission_progress_view.py new file mode 100644 index 0000000000..66544556c0 --- /dev/null +++ b/backend/audit/test_submission_progress_view.py @@ -0,0 +1,120 @@ +from pathlib import Path +from django.contrib.auth import get_user_model +from django.test import Client, TestCase +from django.urls import reverse +from model_bakery import baker + +from audit.cross_validation import ( + sac_validation_shape, + submission_progress_check, +) +from audit.cross_validation.naming import SECTION_NAMES +from .models import Access, SingleAuditChecklist +from .test_views import _load_json + + +AUDIT_JSON_FIXTURES = Path(__file__).parent / "fixtures" / "json" +User = get_user_model() + + +class SubmissionProgressViewTests(TestCase): + """ + The page shows information about a submission and conditionally displays links/other + affordances for individual sections. + """ + + def setUp(self): + self.user = baker.make(User) + self.sac = baker.make(SingleAuditChecklist) + self.client = Client() + + def test_login_required(self): + """When an unauthenticated request is made""" + + response = self.client.post( + reverse( + "audit:SubmissionProgress", + kwargs={"report_id": "12345"}, + ) + ) + + self.assertEqual(response.status_code, 403) + + def test_phrase_in_page(self): + """Check for 'General Information form'.""" + baker.make(Access, user=self.user, sac=self.sac) + self.client.force_login(user=self.user) + phrase = "General Information form" + res = self.client.get( + reverse( + "audit:SubmissionProgress", kwargs={"report_id": self.sac.report_id} + ) + ) + self.assertIn(phrase, res.content.decode("utf-8")) + + def test_submission_progress_check_geninfo_only(self): + """ + Check the function containing the logic around which sections are required. + + If the conditional questions all have negative answers and data is absent for + the rest, return the appropriate shape. + """ + filename = "general-information--test0001test--simple-pass.json" + info = _load_json(AUDIT_JSON_FIXTURES / filename) + sac = baker.make(SingleAuditChecklist, general_information=info) + shaped_sac = sac_validation_shape(sac) + result = submission_progress_check(shaped_sac, sar=None, crossval=False) + self.assertEqual(result["general_information"]["display"], "complete") + self.assertTrue(result["general_information"]["completed"]) + conditional_keys = ( + "additional_ueis", + "additional_eins", + "secondary_auditors", + ) + for key in conditional_keys: + self.assertEqual(result[key]["display"], "inactive") + self.assertFalse(result["complete"]) + baker.make(Access, user=self.user, sac=sac) + self.client.force_login(user=self.user) + res = self.client.get( + reverse("audit:SubmissionProgress", kwargs={"report_id": sac.report_id}) + ) + phrases = ( + "Upload the Additional UEIs workbook", + "Upload the Additional EINs workbook", + "Upload the Secondary Auditors workbook", + ) + for phrase in phrases: + self.assertNotIn(phrase, res.content.decode("utf-8")) + + def test_submission_progress_check_simple_pass(self): + """ + Check the function containing the logic around which sections are required. + + If the conditional questions all have negative answers and data is present for + the rest, return the appropriate shape. + + + """ + filename = "general-information--test0001test--simple-pass.json" + info = _load_json(AUDIT_JSON_FIXTURES / filename) + addl_sections = {} + for section_name, guide in SECTION_NAMES.items(): + camel_name = guide.camel_case + addl_sections[section_name] = {camel_name: "whatever"} + addl_sections["federal_awards"] = {"FederalAwards": {"federal_awards": []}} + addl_sections["general_information"] = info + del addl_sections["single_audit_report"] + sac = baker.make(SingleAuditChecklist, **addl_sections) + shaped_sac = sac_validation_shape(sac) + result = submission_progress_check(shaped_sac, sar=True, crossval=False) + self.assertEqual(result["general_information"]["display"], "complete") + self.assertTrue(result["general_information"]["completed"]) + conditional_keys = ( + "additional_ueis", + "additional_eins", + "secondary_auditors", + ) + for key in conditional_keys: + self.assertEqual(result[key]["display"], "inactive") + self.assertTrue(result["complete"]) diff --git a/backend/audit/test_views.py b/backend/audit/test_views.py index ce1ea12c2d..1fd688146e 100644 --- a/backend/audit/test_views.py +++ b/backend/audit/test_views.py @@ -3,12 +3,11 @@ from tempfile import NamedTemporaryFile from unittest.mock import patch -from django.test import TestCase +from django.test import Client, TestCase from django.contrib.auth import get_user_model from django.core.files.uploadedfile import SimpleUploadedFile from django.urls import reverse -from django.test import Client from model_bakery import baker from openpyxl import load_workbook @@ -38,8 +37,7 @@ fake_auditee_certification, ) from .models import Access, SingleAuditChecklist, SingleAuditReportFile -from .views import MySubmissions, submission_progress_check -from .cross_validation.sac_validation_shape import sac_validation_shape, snake_to_camel +from .views import MySubmissions User = get_user_model() @@ -1104,103 +1102,3 @@ def test_valid_file_upload_for_notes_to_sefa(self, mock_scan_file): notes_to_sefa_entries["note_title"], test_data[0]["note_title"], ) - - -class SubmissionProgressViewTests(TestCase): - """ - The page shows information about a submission and conditionally displays links/other - affordances for individual sections. - """ - - def setUp(self): - self.user = baker.make(User) - self.sac = baker.make(SingleAuditChecklist) - self.client = Client() - - def test_login_required(self): - """When an unauthenticated request is made""" - - response = self.client.post( - reverse( - "audit:SubmissionProgress", - kwargs={"report_id": "12345"}, - ) - ) - - self.assertEqual(response.status_code, 403) - - def test_phrase_in_page(self): - """Check for 'General information form'.""" - baker.make(Access, user=self.user, sac=self.sac) - self.client.force_login(user=self.user) - phrase = "General information form" - res = self.client.get( - reverse( - "audit:SubmissionProgress", kwargs={"report_id": self.sac.report_id} - ) - ) - self.assertIn(phrase, res.content.decode("utf-8")) - - def test_submission_progress_check_geninfo_only(self): - """ - Check the function containing the logic around which sections are required. - - If the conditional questions all have negative answers and data is absent for - the rest, return the appropriate shape. - """ - filename = "general-information--test0001test--simple-pass.json" - info = _load_json(AUDIT_JSON_FIXTURES / filename) - sac = baker.make(SingleAuditChecklist, general_information=info) - shaped_sac = sac_validation_shape(sac) - result = submission_progress_check(shaped_sac, sar=None, crossval=False) - self.assertEqual(result["general_information"]["display"], "complete") - self.assertTrue(result["general_information"]["completed"]) - conditional_keys = ( - "additional_ueis", - "additional_eins", - "secondary_auditors", - ) - for key in conditional_keys: - self.assertEqual(result[key]["display"], "hidden") - self.assertFalse(result["complete"]) - baker.make(Access, user=self.user, sac=sac) - self.client.force_login(user=self.user) - res = self.client.get( - reverse("audit:SubmissionProgress", kwargs={"report_id": sac.report_id}) - ) - phrases = ( - "Upload the Additional UEIs workbook", - "Upload the Additional EINs workbook", - "Upload the Secondary Auditors workbook", - ) - for phrase in phrases: - self.assertNotIn(phrase, res.content.decode("utf-8")) - - def test_submission_progress_check_simple_pass(self): - """ - Check the function containing the logic around which sections are required. - - If the conditional questions all have negative answers and data is present for - the rest, return the appropriate shape. - - - """ - filename = "general-information--test0001test--simple-pass.json" - info = _load_json(AUDIT_JSON_FIXTURES / filename) - addl_sections = {} - for section_name, camel_name in snake_to_camel.items(): - addl_sections[section_name] = {camel_name: "whatever"} - addl_sections["general_information"] = info - sac = baker.make(SingleAuditChecklist, **addl_sections) - shaped_sac = sac_validation_shape(sac) - result = submission_progress_check(shaped_sac, sar=None, crossval=False) - self.assertEqual(result["general_information"]["display"], "complete") - self.assertTrue(result["general_information"]["completed"]) - conditional_keys = ( - "additional_ueis", - "additional_eins", - "secondary_auditors", - ) - for key in conditional_keys: - self.assertEqual(result[key]["display"], "hidden") - self.assertTrue(result["complete"]) diff --git a/backend/audit/viewlib/__init__.py b/backend/audit/viewlib/__init__.py index d38e59da02..99d9073af0 100644 --- a/backend/audit/viewlib/__init__.py +++ b/backend/audit/viewlib/__init__.py @@ -1,5 +1,12 @@ +from .submission_progress_view import ( # noqa + SubmissionProgressView, + submission_progress_check, +) + from .upload_report_view import UploadReportView +# In case we want to iterate through all the views for some reason: views = [ + SubmissionProgressView, UploadReportView, ] diff --git a/backend/audit/viewlib/submission_progress_view.py b/backend/audit/viewlib/submission_progress_view.py new file mode 100644 index 0000000000..44abcf9474 --- /dev/null +++ b/backend/audit/viewlib/submission_progress_view.py @@ -0,0 +1,159 @@ +from django.core.exceptions import PermissionDenied +from django.shortcuts import render +from django.views import generic +from audit.cross_validation import ( + naming, + sac_validation_shape, + submission_progress_check, +) +from audit.mixins import ( + SingleAuditChecklistAccessRequiredMixin, +) +from audit.models import ( + SingleAuditChecklist, + SingleAuditReportFile, +) + + +# Turn the named tuples into dicts because Django templates work with dicts: +SECTIONS_NAMING = {k: v._asdict() for k, v in naming.SECTION_NAMES.items()} + +# The info for the submission page sections: +SECTIONS_PAGE = { + "general_information": { + "edit_text": f"Edit the {SECTIONS_NAMING['general_information']['friendly_title']}", + "text": "Enter general information about the single audit submission, such as the audit type and fiscal period. This is also where you'll list the primary auditor and auditee contacts.", + }, + "audit_information": { + "edit_text": f"Edit the {SECTIONS_NAMING['audit_information']['friendly_title']}", + "text": "Select the status of the financial statements and federal programs covered by your single audit.", + }, + "single_audit_report": { + "edit_text": f"Edit the {SECTIONS_NAMING['single_audit_report']['friendly_title']}", + "text": "Upload the audit report. This should be a single PDF that is unlocked and machine-readable.", + }, + "federal_awards": { + "edit_text": f"Edit the {SECTIONS_NAMING['federal_awards']['friendly_title']}", + "text": "For each federal award received, you'll need the financial and agency details. This is also where you list the number of audit findings.", + "text_nb": "You must complete this workbook first.", + }, + "notes_to_sefa": { + "edit_text": f"Edit the {SECTIONS_NAMING['notes_to_sefa']['friendly_title']}", + "text": "This workbook covers notes on the Schedule of Expenditures of Federal Awards (SEFA). Enter the information of each Federal awards program from which the auditee received funds, even if they don't have audit findings.", + }, + "findings_uniform_guidance": { + "edit_text": f"Edit the {SECTIONS_NAMING['findings_uniform_guidance']['friendly_title']}", + "text": "Complete this workbook using the Summary Schedule of Prior Audit Findings and the information in the financial statement audit. If there are no audit findings listed in Workbook 1: Federal Awards, only enter the auditee EIN in this workbook and upload.", + }, + "findings_text": { + "edit_text": f"Edit the {SECTIONS_NAMING['findings_text']['friendly_title']}", + "text": "Enter the full text of the audit finding, listing each finding only once, even if they relate to more than one program. Include the audit finding reference number for each. If there are no audit findings listed in Workbook 1: Federal Awards, only enter the auditee EIN in this workbook and upload.", + }, + "corrective_action_plan": { + "edit_text": f"Edit the {SECTIONS_NAMING['corrective_action_plan']['friendly_title']}", + "text": "This information should match the data you entered in the Findings Text workbook. You only need to enter plans for findings once if they relate to more than one program. If there are no audit findings listed in Workbook 1: Federal Awards, only enter the auditee EIN in this workbook and upload.", + }, + "additional_ueis": { + "edit_text": f"Edit the {SECTIONS_NAMING['additional_ueis']['friendly_title']}", + "text": "This workbook is only necessary if the audit report covers multiple UEIs. List the additional UEIs covered by the audit, excluding the primary UEI.", + }, + "secondary_auditors": { + "edit_text": f"Edit the {SECTIONS_NAMING['secondary_auditors']['friendly_title']}", + "text": "This workbook is only necessary if multiple auditors did the audit work.", + }, + "additional_eins": { + "edit_text": f"Edit the {SECTIONS_NAMING['additional_eins']['friendly_title']}", + "text": "This workbook is only necessary if the audit report covers multiple EINs. List the additional EINs covered by the audit, excluding the primary EIN.", + }, +} + +# Combine the submission page info with the naming info: +SECTIONS_BASE = { + k: v | SECTIONS_PAGE[k] for k, v in SECTIONS_NAMING.items() if k in SECTIONS_PAGE +} + + +class SubmissionProgressView(SingleAuditChecklistAccessRequiredMixin, generic.View): + """ + Display information about and the current status of the sections of the submission, + including links to the pages for the sections. + + The following sections have three states, rather than two: + + incomplete + + Findings Uniform Guidance + + Findings Text + + Corrective Action Plan + + Additionai UEIs + + Additionai EINs + + Secondary Auditors + + The states are: + + + hidden + + incomplete + + complete + + In each case, they are hidden if the corresponding question in the General + Information form has been answered with a negative response. + """ + + def get(self, request, *args, **kwargs): + report_id = kwargs["report_id"] + + try: + sac = SingleAuditChecklist.objects.get(report_id=report_id) + try: + sar = SingleAuditReportFile.objects.filter(sac_id=sac.id).latest( + "date_created" + ) + except SingleAuditReportFile.DoesNotExist: + sar = None + + shaped_sac = sac_validation_shape(sac) + subcheck = submission_progress_check(shaped_sac, sar, crossval=False) + # Update with the view-specific info from SECTIONS_BASE: + for key, value in SECTIONS_BASE.items(): + subcheck[key] = subcheck[key] | value + + context = { + "single_audit_checklist": { + "created": True, + "created_date": sac.date_created, + "created_by": sac.submitted_by, + "completed": False, + "completed_date": None, + "completed_by": None, + }, + "pre_submission_validation": { + "completed": sac.submission_status == "ready_for_certification", + "completed_date": None, + "completed_by": None, + # We want the user to always be able to run this check: + "enabled": True, + }, + "certification": { + "auditor_certified": bool(sac.auditor_certification), + "auditor_enabled": sac.submission_status + == "ready_for_certification", + "auditee_certified": bool(sac.auditee_certification), + "auditee_enabled": sac.submission_status == "auditor_certified", + }, + "submission": { + "completed": sac.submission_status == "submitted", + "completed_date": None, + "completed_by": None, + "enabled": sac.submission_status == "auditee_certified", + }, + "report_id": report_id, + "auditee_name": sac.auditee_name, + "auditee_uei": sac.auditee_uei, + "user_provided_organization_type": sac.user_provided_organization_type, + } + context = context | subcheck + + return render( + request, "audit/submission_checklist/submission-checklist.html", context + ) + except SingleAuditChecklist.DoesNotExist as err: + raise PermissionDenied("You do not have access to this audit.") from err diff --git a/backend/audit/views.py b/backend/audit/views.py index afb2e7489e..e8a26bfff7 100644 --- a/backend/audit/views.py +++ b/backend/audit/views.py @@ -17,9 +17,8 @@ SP_FRAMEWORK_BASIS, SP_FRAMEWORK_OPINIONS, ) -from .fixtures.excel import FORM_SECTIONS, UNKNOWN_WORKBOOK +from audit.fixtures.excel import FORM_SECTIONS, UNKNOWN_WORKBOOK -from audit.cross_validation import sac_validation_shape, submission_progress_check from audit.excel import ( extract_additional_ueis, extract_additional_eins, @@ -63,7 +62,11 @@ validate_notes_to_sefa_json, validate_secondary_auditors_json, ) -from audit.viewlib import UploadReportView # noqa +from audit.viewlib import ( # noqa + SubmissionProgressView, + UploadReportView, + submission_progress_check, +) logging.basicConfig( @@ -694,85 +697,6 @@ def post(self, request, *args, **kwargs): raise PermissionDenied("You do not have access to this audit.") -class SubmissionProgressView(SingleAuditChecklistAccessRequiredMixin, generic.View): - """ - Display information about and the current status of the sections of the submission, - including links to the pages for the sections. - - The following sections have three states, rather than two: - - + Additionai UEIs - + Additionai EINs - + Secondary Auditors - - The states are: - - + hidden - + incomplete - + complete - - In each case, they are hidden if the corresponding question in the General - Information form has been answered with a negative response. - """ - - def get(self, request, *args, **kwargs): - report_id = kwargs["report_id"] - - try: - sac = SingleAuditChecklist.objects.get(report_id=report_id) - try: - sar = SingleAuditReportFile.objects.filter(sac_id=sac.id).latest( - "date_created" - ) - except SingleAuditReportFile.DoesNotExist: - sar = None - - shaped_sac = sac_validation_shape(sac) - subcheck = submission_progress_check(shaped_sac, sar, crossval=False) - - context = { - "single_audit_checklist": { - "created": True, - "created_date": sac.date_created, - "created_by": sac.submitted_by, - "completed": False, - "completed_date": None, - "completed_by": None, - }, - "pre_submission_validation": { - "completed": sac.submission_status == "ready_for_certification", - "completed_date": None, - "completed_by": None, - # We want the user to always be able to run this check: - "enabled": True, - }, - "certification": { - "auditor_certified": bool(sac.auditor_certification), - "auditor_enabled": sac.submission_status - == "ready_for_certification", - "auditee_certified": bool(sac.auditee_certification), - "auditee_enabled": sac.submission_status == "auditor_certified", - }, - "submission": { - "completed": sac.submission_status == "submitted", - "completed_date": None, - "completed_by": None, - "enabled": sac.submission_status == "auditee_certified", - }, - "report_id": report_id, - "auditee_name": sac.auditee_name, - "auditee_uei": sac.auditee_uei, - "user_provided_organization_type": sac.user_provided_organization_type, - } - context = context | subcheck - - return render( - request, "audit/submission_checklist/submission-checklist.html", context - ) - except SingleAuditChecklist.DoesNotExist as err: - raise PermissionDenied("You do not have access to this audit.") from err - - class AuditInfoFormView(SingleAuditChecklistAccessRequiredMixin, generic.View): def get(self, request, *args, **kwargs): report_id = kwargs["report_id"] diff --git a/backend/cypress/e2e/full-submission.cy.js b/backend/cypress/e2e/full-submission.cy.js index 968baab950..010b0b5f34 100644 --- a/backend/cypress/e2e/full-submission.cy.js +++ b/backend/cypress/e2e/full-submission.cy.js @@ -1,5 +1,6 @@ import { testCrossValidation } from '../support/cross-validation.js'; import { testLoginGovLogin } from '../support/login-gov.js'; +import { testLogoutGov } from '../support/logout-gov.js'; import { testValidAccess } from '../support/check-access.js'; import { testValidEligibility } from '../support/check-eligibility.js'; import { testValidAuditeeInfo } from '../support/auditee-info.js'; @@ -8,12 +9,18 @@ import { testAuditInformationForm } from '../support/audit-info-form.js'; import { testPdfAuditReport } from '../support/report-pdf.js'; import { testAuditorCertification } from '../support/auditor-certification.js'; import { testAuditeeCertification } from '../support/auditee-certification.js'; -import { testWorkbookFederalAwards, - testWorkbookNotesToSEFA, - testWorkbookFindingsUniformGuidance, - testWorkbookFindingsText, - testWorkbookCorrectiveActionPlan, - testWorkbookAdditionalUEIs } from '../support/workbook-uploads.js'; +import { + testWorkbookFederalAwards, + testWorkbookNotesToSEFA, + testWorkbookFindingsUniformGuidance, + testWorkbookFindingsText, + testWorkbookCorrectiveActionPlan, + testWorkbookAdditionalUEIs, +} from '../support/workbook-uploads.js'; + +const LOGIN_TEST_EMAIL_AUDITEE = Cypress.env('LOGIN_TEST_EMAIL_AUDITEE'); +const LOGIN_TEST_PASSWORD_AUDITEE = Cypress.env('LOGIN_TEST_PASSWORD_AUDITEE'); +const LOGIN_TEST_OTP_SECRET_AUDITEE = Cypress.env('LOGIN_TEST_OTP_SECRET_AUDITEE'); describe('Full audit submission', () => { before(() => { @@ -79,21 +86,35 @@ describe('Full audit submission', () => { cy.get(".usa-link").contains("Pre-submission validation").click(); testCrossValidation(); - // Second, auditor certification + // Auditor certification cy.get(".usa-link").contains("Auditor Certification").click(); testAuditorCertification(); - // Third, auditee certification - cy.get(".usa-link").contains("Auditee Certification").click(); - testAuditeeCertification(); - // The same as auditor certification, with different checkboxes. + // Auditee certification + cy.url().then(url => { + // Grab the report ID from the URL + const reportId = url.split('/').pop(); + + testLogoutGov(); + + // Login as Auditee + testLoginGovLogin( + LOGIN_TEST_EMAIL_AUDITEE, + LOGIN_TEST_PASSWORD_AUDITEE, + LOGIN_TEST_OTP_SECRET_AUDITEE + ); + + cy.visit(`/audit/submission-progress/${reportId}`); + + cy.get(".usa-link").contains("Auditee Certification").click(); + testAuditeeCertification(); + }) // Uncomment this block when ready to implement the certification steps. /* // Finally, submit for processing. cy.get(".usa-link").contains("Submit to the FAC for processing").click(); // This will probably take you back to the homepage, where the audit is now oof status "submitted". - */ }); }); diff --git a/backend/cypress/support/check-access.js b/backend/cypress/support/check-access.js index a9934b902a..d10b94afac 100644 --- a/backend/cypress/support/check-access.js +++ b/backend/cypress/support/check-access.js @@ -8,9 +8,11 @@ const accessFields = [ export function addValidInfo(field) { const fieldType = field.split('_').pop(); + const email = field.includes('auditee') ? Cypress.env('LOGIN_TEST_EMAIL_AUDITEE') : Cypress.env('LOGIN_TEST_EMAIL'); + cy.get(field) .clear() - .type(fieldType === 'email' ? Cypress.env('LOGIN_TEST_EMAIL') : 'Percy A. Person') + .type(fieldType === 'email' ? email : 'Percy A. Person') .blur(); } diff --git a/backend/cypress/support/commands.js b/backend/cypress/support/commands.js index 8eb5e57324..556839458e 100644 --- a/backend/cypress/support/commands.js +++ b/backend/cypress/support/commands.js @@ -81,7 +81,12 @@ import 'cypress-file-upload'; // import { testLoginGovLogin } from './login-gov.js'; +import { testLogoutGov } from './logout-gov.js'; Cypress.Commands.add('login', () => { testLoginGovLogin(); }) + +Cypress.Commands.add('logout',() => { + testLogoutGov(); +}) diff --git a/backend/cypress/support/login-gov.js b/backend/cypress/support/login-gov.js index 24101c364b..3f6fd0d49f 100644 --- a/backend/cypress/support/login-gov.js +++ b/backend/cypress/support/login-gov.js @@ -3,7 +3,8 @@ const LOGIN_TEST_EMAIL = Cypress.env('LOGIN_TEST_EMAIL'); const LOGIN_TEST_PASSWORD = Cypress.env('LOGIN_TEST_PASSWORD'); const LOGIN_TEST_OTP_SECRET = Cypress.env('LOGIN_TEST_OTP_SECRET'); -export function testLoginGovLogin() { +export function testLoginGovLogin( + email=LOGIN_TEST_EMAIL, password=LOGIN_TEST_PASSWORD, secret=LOGIN_TEST_OTP_SECRET) { cy.get('a.usa-button.sign-in-button').click(); cy.get('button.usa-button.sign-in-button') .should('contain.text', 'Authenticate with Login.gov') @@ -11,17 +12,17 @@ export function testLoginGovLogin() { cy.origin( 'https://idp.int.identitysandbox.gov/', { - args: { LOGIN_TEST_EMAIL, LOGIN_TEST_PASSWORD, LOGIN_TEST_OTP_SECRET }, + args: { email, password, secret }, }, - ({ LOGIN_TEST_EMAIL, LOGIN_TEST_PASSWORD, LOGIN_TEST_OTP_SECRET }) => { - cy.get('#user_email').type(LOGIN_TEST_EMAIL); - cy.get('input[id^="password-toggle-input-"]').type(LOGIN_TEST_PASSWORD); + ({ email, password, secret }) => { + cy.get('#user_email').type(email); + cy.get('input[id^="password-toggle-input-"]').type(password); cy.get('lg-submit-button > .usa-button').click(); cy.url().should( 'contain', 'https://idp.int.identitysandbox.gov/login/two_factor/authenticator' ); - cy.task('generateOTP', LOGIN_TEST_OTP_SECRET).then((token) => { + cy.task('generateOTP', secret).then((token) => { cy.get('input.one-time-code-input__input').type(token); }); cy.get('lg-submit-button > .usa-button').click(); diff --git a/backend/cypress/support/logout-gov.js b/backend/cypress/support/logout-gov.js new file mode 100644 index 0000000000..1548c4b635 --- /dev/null +++ b/backend/cypress/support/logout-gov.js @@ -0,0 +1,11 @@ +export function testLogoutGov() { + cy.get('button').contains('Menu').click(); + cy.get('button').contains('Sign out').click(); + cy.origin( + 'https://idp.int.identitysandbox.gov/', + {}, + () => { + cy.contains('Yes, sign out of Login.gov').click(); + } + ); +} diff --git a/backend/cypress/support/workbook-uploads.js b/backend/cypress/support/workbook-uploads.js index f8470dc840..15a0ef0a60 100644 --- a/backend/cypress/support/workbook-uploads.js +++ b/backend/cypress/support/workbook-uploads.js @@ -64,7 +64,7 @@ export function testWorkbookFindingsText(will_intercept = true) { export function testWorkbookCorrectiveActionPlan(will_intercept = true) { testWorkbookUpload( '/audit/excel/corrective-action-plan/', - '#file-input-CAP-xlsx', + '#file-input-cap-xlsx', 'test_workbooks/corrective-action-plan-workbook.xlsx', will_intercept ) diff --git a/backend/report_submission/urls.py b/backend/report_submission/urls.py index 8bf552abee..5ca3d09197 100644 --- a/backend/report_submission/urls.py +++ b/backend/report_submission/urls.py @@ -33,7 +33,7 @@ for page_id in upload_page_ids: urlpatterns.append( path( - "{}/".format(page_id), + f"{page_id.lower()}/", views.UploadPageView.as_view(), name=page_id, ) diff --git a/backend/report_submission/views.py b/backend/report_submission/views.py index f15f9429df..d8c7424e0e 100644 --- a/backend/report_submission/views.py +++ b/backend/report_submission/views.py @@ -244,8 +244,8 @@ def get(self, request, *args, **kwargs): "workbook_url": workbook_base_url + "federal-awards-audit-findings-text-workbook.xlsx", }, - "CAP": { - "view_id": "CAP", + "cap": { + "view_id": "cap", "view_name": "Corrective Action Plan (CAP)", "instructions": "Enter your CAP text using the provided worksheet.", "DB_id": "corrective_action_plan", diff --git a/backend/static/js/globals.js b/backend/static/js/globals.js index f3cbe9d0a1..11eff355f0 100644 --- a/backend/static/js/globals.js +++ b/backend/static/js/globals.js @@ -8,5 +8,5 @@ export const UPLOAD_URLS = { 'secondary-auditors': 'secondary-auditors', 'additional-ueis': 'additional-ueis', 'additional-eins': 'additional-eins', - CAP: 'corrective-action-plan', + cap: 'corrective-action-plan', }; diff --git a/backend/templates/400.html b/backend/templates/400.html new file mode 100644 index 0000000000..1a5ee13651 --- /dev/null +++ b/backend/templates/400.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} +{% block content %} +
    +
    +

    400 Error: Bad Request

    +
    +

    It seems your browser is not responding properly. Try refreshing this page.

    +
    + {% if exception %}

    Error code: {{ exception }}

    {% endif %} +
    +
    +{% endblock content %} diff --git a/backend/templates/401.html b/backend/templates/401.html new file mode 100644 index 0000000000..750ca47be1 --- /dev/null +++ b/backend/templates/401.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} +{% block content %} +
    +
    +

    401 Error: Unauthorized

    +
    +

    + Pleasse sign in to view this page +

    + Sign in +
    + {% if exception %}

    Error code: {{ exception }}

    {% endif %} +
    +
    +{% endblock content %} diff --git a/backend/templates/403.html b/backend/templates/403.html index 8ebf86fd8b..c9d49f9337 100644 --- a/backend/templates/403.html +++ b/backend/templates/403.html @@ -1,17 +1,15 @@ {% extends "base.html" %} {% block content %} -
    -

    Access denied

    -
    - {% if exception %} -

    - {{ exception }} -

    - {% else %} -

    - You do not have permission to view this resource. -

    - {% endif %} -
    -
    +
    +
    +

    403 Error: Access Forbidden

    +
    +

    + You are not authorized to view this page. If you believe you’ve received this message in error, please contact the submission package owner to ensure you’re given user access. +

    +

    Try returning to the previous page.

    +
    + {% if exception %}

    Error code: {{ exception }}

    {% endif %} +
    +
    {% endblock content %} diff --git a/backend/templates/404.html b/backend/templates/404.html new file mode 100644 index 0000000000..09d053072a --- /dev/null +++ b/backend/templates/404.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} +{% block content %} +
    +
    +

    404 Error: Not Found

    +
    +

    We can't find the page you are looking for.

    +

    Try returning to the previous page.

    +
    + {% if exception %}

    Error code: {{ exception }}

    {% endif %} +
    +
    +{% endblock content %} diff --git a/backend/templates/500.html b/backend/templates/500.html new file mode 100644 index 0000000000..f3e8d0a090 --- /dev/null +++ b/backend/templates/500.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} +{% block content %} +
    +
    +

    500 Error: Internal Server Error

    +
    +

    Something went wrong. Try refreshing the page, or come back later.

    +
    + {% if exception %}

    Error code: {{ exception }}

    {% endif %} +
    +
    +{% endblock content %} diff --git a/docs/testing.md b/docs/testing.md index 642538542d..5642fd0cd6 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -145,7 +145,10 @@ in files in [backend/cypress/e2e/](/backend/cypress/e2e). Run cypress with `npx To pass that email address, password, and secret key into Cypress, set the environment variables `CYPRESS_LOGIN_TEST_EMAIL`, `CYPRESS_LOGIN_TEST_PASSWORD`, and - `CYPRESS_LOGIN_TEST_OTP_SECRET`. Obviously, do not store these values in our + `CYPRESS_LOGIN_TEST_OTP_SECRET`. You'll need similar credentials for + `CYPRESS_LOGIN_TEST_EMAIL_AUDITEE`, `CYPRESS_LOGIN_TEST_PASSWORD_AUDITEE`, and + `CYPRESS_LOGIN_TEST_OTP_SECRET_AUDITEE`. These can be the same values, but ideally + they'll belong to a different account. Obviously, do not store these values in our Github repository. To use them in a Github Actions workflow, use the [Github Actions secrets store](https://docs.github.com/en/actions/security-guides/encrypted-secrets)