+
+ {% 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
+
+ {% 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)