Skip to content

Commit

Permalink
Merge pull request #1955 from GSA-TTS/main
Browse files Browse the repository at this point in the history
  • Loading branch information
jadudm authored Aug 26, 2023
2 parents 55e8370 + aa9b1d5 commit 128fa38
Show file tree
Hide file tree
Showing 29 changed files with 684 additions and 663 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/regression-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:*
Expand Down
54 changes: 43 additions & 11 deletions backend/audit/cross_validation/naming.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -89,17 +89,17 @@ 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(
all_caps="FINDINGS_UNIFORM_GUIDANCE",
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,
Expand All @@ -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",
Expand All @@ -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
27 changes: 9 additions & 18 deletions backend/audit/cross_validation/sac_validation_shape.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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,
Expand Down
145 changes: 65 additions & 80 deletions backend/audit/cross_validation/submission_progress_check.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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:
Expand All @@ -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 [
Expand All @@ -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}}
2 changes: 1 addition & 1 deletion backend/audit/templates/audit/audit-info-form.html
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ <h1 class="margin-top-6">Federal programs</h1>
</fieldset>
<fieldset class="usa-fieldset margin-top-2">
<legend>
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) <abbr title="required" class="usa-hint usa-hint--required">*</abbr>
What is the dollar threshold to distinguish Type A and Type B programs? (Uniform Guidance § 200.518(b)(1)) <abbr title="required" class="usa-hint usa-hint--required">*</abbr>
</legend>
<div class="grid-row">
<label class="margin-top-2 margin-right-1">$</label>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
{% comment %}
<div> 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 %}
<div class="usa-icon-list__icon text-success-darker">
<svg class="usa-icon margin-right-3" aria-hidden="true" role="img">
{% uswds_sprite "check_circle" %}
</svg>
</div>
<div class="usa-icon-list__icon text-success-darker">
<svg class="usa-icon margin-right-3" aria-hidden="true" role="img">
{% uswds_sprite "check_circle" %}
</svg>
</div>
{% elif display == 'incomplete' %}
<div class="usa-icon-list__icon ">
<svg class="usa-icon margin-right-3" aria-hidden="true" role="img">
{% comment %} {% uswds_sprite "radio_button_unchecked" %} {% endcomment %}
</svg>
</div>
{% else %}
<div class="usa-icon-list__icon text-success-darker">
<svg class="usa-icon margin-right-3" aria-hidden="true" role="img">
{% comment %} {% uswds_sprite "radio_button_unchecked" %} {% endcomment %}
</svg>
</div>
<div class="usa-icon-list__icon text-disabled-dark">
<svg class="usa-icon margin-right-3" aria-hidden="true" role="img">
{% comment %} {% uswds_sprite "radio_button_unchecked" %} {% endcomment %}
</svg>
</div>
{% endif %}
Loading

0 comments on commit 128fa38

Please sign in to comment.