Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2024-08-22 | MAIN --> PROD | DEV (e1605f2) --> STAGING #4211

Merged
merged 2 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,15 @@
"program": {
"federal_agency_prefix": "42",
"three_digit_extension": "RD",
"additional_award_identification": 1234,
"additional_award_identification": "1234",
"program_name": "FLOWER DREAMING",
"is_major": "N",
"audit_report_type": "",
"number_of_audit_findings": 0,
"amount_expended": 5000000,
"federal_program_total": 5000000
},
"loan_or_loan_guarantee": {
"is_guaranteed": "N",
"loan_balance_at_audit_period_end": 0
"is_guaranteed": "N"
},
"direct_or_indirect_award": {
"is_direct": "N",
Expand Down
51 changes: 42 additions & 9 deletions backend/audit/models/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError

from django.utils.translation import gettext_lazy as _
from django.utils import timezone as django_timezone

from django_fsm import FSMField, RETURN_VALUE, transition

import audit.cross_validation
from audit.cross_validation.naming import SECTION_NAMES
from audit.intake_to_dissemination import IntakeToDissemination
from audit.validators import (
validate_additional_ueis_json,
Expand All @@ -34,6 +36,7 @@
validate_audit_information_json,
validate_component_page_numbers,
)
from audit.utils import FORM_SECTION_HANDLERS
from support.cog_over import compute_cog_over, record_cog_assignment
from .submission_event import SubmissionEvent

Expand Down Expand Up @@ -430,18 +433,19 @@ def validate_full(self):
"""
Full validation, intended for use when the user indicates that the
submission is finished.

Currently a stub, but eventually will call each of the individual
section validation routines and then validate_cross.
"""
cross_result = self.validate_cross()
individual_result = self.validate_individually()
full_result = {}

validation_methods = []
errors = [f(self) for f in validation_methods]

if errors:
return {"errors": errors}
if "errors" in cross_result:
full_result = cross_result
if "errors" in individual_result:
full_result["errors"].extend(individual_result["errors"])
elif "errors" in individual_result:
full_result = individual_result

return self.validate_cross()
return full_result

def validate_cross(self):
"""
Expand Down Expand Up @@ -469,6 +473,35 @@ def validate_cross(self):
return {"errors": errors, "data": shaped_sac}
return {}

def validate_individually(self):
"""
Runs the individual workbook validations, returning generic errors as a
list of strings. Ignores workbooks that haven't been uploaded yet.
"""
errors = []
result = {}

for section, section_handlers in FORM_SECTION_HANDLERS.items():
validation_method = section_handlers["validator"]
section_name = section_handlers["field_name"]
audit_data = getattr(self, section_name)

try:
validation_method(audit_data)
except ValidationError as err:
# err.error_list will be [] if the workbook wasn't uploaded yet
if err.error_list:
errors.append(
{
"error": f"The {SECTION_NAMES[section_name].friendly} workbook contains validation errors and will need to be re-uploaded. This is likely caused by changes made to our validations in the time since it was originally uploaded."
}
)

if errors:
result = {"errors": errors}

return result

@transition(
field="submission_status",
source=STATUS.IN_PROGRESS,
Expand Down
21 changes: 16 additions & 5 deletions backend/audit/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,11 +293,12 @@ def test_post_redirect(self):
"audit_information": _fake_audit_information(),
"federal_awards": _load_json(AUDIT_JSON_FIXTURES / awardsfile),
"general_information": _load_json(AUDIT_JSON_FIXTURES / geninfofile),
"submission_status": STATUSES.AUDITEE_CERTIFIED,
"submission_status": STATUSES.IN_PROGRESS, # Temporarily required for SAR creation below
}
sac_data["notes_to_sefa"]["NotesToSefa"]["accounting_policies"] = "Exhaustive"
sac_data["notes_to_sefa"]["NotesToSefa"]["is_minimis_rate_used"] = "Y"
sac_data["notes_to_sefa"]["NotesToSefa"]["rate_explained"] = "At great length"
sac_data["report_id"] = _mock_gen_report_id()
user, sac = _make_user_and_sac(**sac_data)

required_statuses = (
Expand All @@ -311,8 +312,10 @@ def test_post_redirect(self):
sac.transition_name.append(rs)
sac.transition_date.append(datetime.now(timezone.utc))

sac.save()
baker.make(SingleAuditReportFile, sac=sac)
baker.make(Access, user=user, sac=sac, role="certifying_auditee_contact")
sac.submission_status = STATUSES.AUDITEE_CERTIFIED
sac.save()

response = _authed_post(
Client(),
Expand Down Expand Up @@ -373,6 +376,9 @@ def test_ready_for_certification(self):
"federal_awards": _load_json(AUDIT_JSON_FIXTURES / awardsfile),
"general_information": _load_json(AUDIT_JSON_FIXTURES / geninfofile),
}
sac_data["notes_to_sefa"]["NotesToSefa"]["accounting_policies"] = "Exhaustive"
sac_data["notes_to_sefa"]["NotesToSefa"]["is_minimis_rate_used"] = "Y"
sac_data["notes_to_sefa"]["NotesToSefa"]["rate_explained"] = "At great length"

sac = SingleAuditChecklist.objects.get(report_id=report_id)
for field, value in sac_data.items():
Expand Down Expand Up @@ -427,6 +433,9 @@ def test_unlock_after_certification(self):
"federal_awards": _load_json(AUDIT_JSON_FIXTURES / awardsfile),
"general_information": _load_json(AUDIT_JSON_FIXTURES / geninfofile),
}
sac_data["notes_to_sefa"]["NotesToSefa"]["accounting_policies"] = "Exhaustive"
sac_data["notes_to_sefa"]["NotesToSefa"]["is_minimis_rate_used"] = "Y"
sac_data["notes_to_sefa"]["NotesToSefa"]["rate_explained"] = "At great length"

sac = SingleAuditChecklist.objects.get(report_id=report_id)
for field, value in sac_data.items():
Expand Down Expand Up @@ -564,11 +573,12 @@ def test_submission(self):
"audit_information": _fake_audit_information(),
"federal_awards": _load_json(AUDIT_JSON_FIXTURES / awardsfile),
"general_information": _load_json(AUDIT_JSON_FIXTURES / geninfofile),
"submission_status": STATUSES.AUDITEE_CERTIFIED,
"submission_status": STATUSES.IN_PROGRESS, # Temporarily required for SAR creation below
}
sac_data["notes_to_sefa"]["NotesToSefa"]["accounting_policies"] = "Exhaustive"
sac_data["notes_to_sefa"]["NotesToSefa"]["is_minimis_rate_used"] = "Y"
sac_data["notes_to_sefa"]["NotesToSefa"]["rate_explained"] = "At great length"
sac_data["report_id"] = _mock_gen_report_id()
user, sac = _make_user_and_sac(**sac_data)

required_statuses = (
Expand All @@ -582,9 +592,10 @@ def test_submission(self):
sac.transition_name.append(rs)
sac.transition_date.append(datetime.now(timezone.utc))

sac.save()

baker.make(SingleAuditReportFile, sac=sac)
baker.make(Access, sac=sac, user=user, role="certifying_auditee_contact")
sac.submission_status = STATUSES.AUDITEE_CERTIFIED
sac.save()

kwargs = {"report_id": sac.report_id}
_authed_post(self.client, user, "audit:Submission", kwargs=kwargs)
Expand Down
66 changes: 66 additions & 0 deletions backend/audit/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
from django.conf import settings

from audit.fixtures.excel import FORM_SECTIONS
from audit.intakelib import (
extract_additional_ueis,
extract_additional_eins,
extract_federal_awards,
extract_corrective_action_plan,
extract_audit_findings_text,
extract_audit_findings,
extract_secondary_auditors,
extract_notes_to_sefa,
)
from audit.validators import (
validate_additional_ueis_json,
validate_additional_eins_json,
validate_corrective_action_plan_json,
validate_federal_award_json,
validate_findings_text_json,
validate_findings_uniform_guidance_json,
validate_notes_to_sefa_json,
validate_secondary_auditors_json,
)


class Util:
@staticmethod
Expand Down Expand Up @@ -63,3 +85,47 @@ def __init__(

def __str__(self):
return f"{self.message} (Error Key: {self.error_key})"


FORM_SECTION_HANDLERS = {
FORM_SECTIONS.FEDERAL_AWARDS: {
"extractor": extract_federal_awards,
"field_name": "federal_awards",
"validator": validate_federal_award_json,
},
FORM_SECTIONS.CORRECTIVE_ACTION_PLAN: {
"extractor": extract_corrective_action_plan,
"field_name": "corrective_action_plan",
"validator": validate_corrective_action_plan_json,
},
FORM_SECTIONS.FINDINGS_UNIFORM_GUIDANCE: {
"extractor": extract_audit_findings,
"field_name": "findings_uniform_guidance",
"validator": validate_findings_uniform_guidance_json,
},
FORM_SECTIONS.FINDINGS_TEXT: {
"extractor": extract_audit_findings_text,
"field_name": "findings_text",
"validator": validate_findings_text_json,
},
FORM_SECTIONS.ADDITIONAL_UEIS: {
"extractor": extract_additional_ueis,
"field_name": "additional_ueis",
"validator": validate_additional_ueis_json,
},
FORM_SECTIONS.ADDITIONAL_EINS: {
"extractor": extract_additional_eins,
"field_name": "additional_eins",
"validator": validate_additional_eins_json,
},
FORM_SECTIONS.SECONDARY_AUDITORS: {
"extractor": extract_secondary_auditors,
"field_name": "secondary_auditors",
"validator": validate_secondary_auditors_json,
},
FORM_SECTIONS.NOTES_TO_SEFA: {
"extractor": extract_notes_to_sefa,
"field_name": "notes_to_sefa",
"validator": validate_notes_to_sefa_json,
},
}
77 changes: 14 additions & 63 deletions backend/audit/views/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,7 @@

from audit.fixtures.excel import FORM_SECTIONS, UNKNOWN_WORKBOOK

from audit.intakelib import (
extract_additional_ueis,
extract_additional_eins,
extract_federal_awards,
extract_corrective_action_plan,
extract_audit_findings_text,
extract_audit_findings,
extract_secondary_auditors,
extract_notes_to_sefa,
)

from audit.forms import (
AuditorCertificationStep1Form,
AuditorCertificationStep2Form,
Expand All @@ -46,17 +37,10 @@
)
from audit.intakelib.exceptions import ExcelExtractionError
from audit.validators import (
validate_additional_ueis_json,
validate_additional_eins_json,
validate_auditee_certification_json,
validate_auditor_certification_json,
validate_corrective_action_plan_json,
validate_federal_award_json,
validate_findings_text_json,
validate_findings_uniform_guidance_json,
validate_notes_to_sefa_json,
validate_secondary_auditors_json,
)
from audit.utils import FORM_SECTION_HANDLERS

from dissemination.remove_workbook_artifacts import remove_workbook_artifacts
from dissemination.file_downloads import get_download_url, get_filename
Expand Down Expand Up @@ -126,49 +110,6 @@ def get(self, request, *args, **kwargs):


class ExcelFileHandlerView(SingleAuditChecklistAccessRequiredMixin, generic.View):
FORM_SECTION_HANDLERS = {
FORM_SECTIONS.FEDERAL_AWARDS: {
"extractor": extract_federal_awards,
"field_name": "federal_awards",
"validator": validate_federal_award_json,
},
FORM_SECTIONS.CORRECTIVE_ACTION_PLAN: {
"extractor": extract_corrective_action_plan,
"field_name": "corrective_action_plan",
"validator": validate_corrective_action_plan_json,
},
FORM_SECTIONS.FINDINGS_UNIFORM_GUIDANCE: {
"extractor": extract_audit_findings,
"field_name": "findings_uniform_guidance",
"validator": validate_findings_uniform_guidance_json,
},
FORM_SECTIONS.FINDINGS_TEXT: {
"extractor": extract_audit_findings_text,
"field_name": "findings_text",
"validator": validate_findings_text_json,
},
FORM_SECTIONS.ADDITIONAL_UEIS: {
"extractor": extract_additional_ueis,
"field_name": "additional_ueis",
"validator": validate_additional_ueis_json,
},
FORM_SECTIONS.ADDITIONAL_EINS: {
"extractor": extract_additional_eins,
"field_name": "additional_eins",
"validator": validate_additional_eins_json,
},
FORM_SECTIONS.SECONDARY_AUDITORS: {
"extractor": extract_secondary_auditors,
"field_name": "secondary_auditors",
"validator": validate_secondary_auditors_json,
},
FORM_SECTIONS.NOTES_TO_SEFA: {
"extractor": extract_notes_to_sefa,
"field_name": "notes_to_sefa",
"validator": validate_notes_to_sefa_json,
},
}

def _create_excel_file(self, file, sac_id, form_section):
excel_file = ExcelFile(
**{
Expand All @@ -194,7 +135,7 @@ def _event_type(self, form_section):
}[form_section]

def _extract_and_validate_data(self, form_section, excel_file, auditee_uei):
handler_info = self.FORM_SECTION_HANDLERS.get(form_section)
handler_info = FORM_SECTION_HANDLERS.get(form_section)
if handler_info is None:
logger.warning("No form section found with name %s", form_section)
raise BadRequest()
Expand All @@ -205,7 +146,7 @@ def _extract_and_validate_data(self, form_section, excel_file, auditee_uei):
return audit_data

def _save_audit_data(self, sac, form_section, audit_data):
handler_info = self.FORM_SECTION_HANDLERS.get(form_section)
handler_info = FORM_SECTION_HANDLERS.get(form_section)
if handler_info is not None:
setattr(sac, handler_info["field_name"], audit_data)
sac.save()
Expand Down Expand Up @@ -774,6 +715,16 @@ def post(self, request, *args, **kwargs):
try:
sac = SingleAuditChecklist.objects.get(report_id=report_id)

errors = sac.validate_full()
if errors:
context = {"report_id": report_id, "errors": errors}

return render(
request,
"audit/cross-validation/cross-validation-results.html",
context,
)

sac.transition_to_submitted()
sac.save(
event_user=request.user, event_type=SubmissionEvent.EventType.SUBMITTED
Expand Down
Loading
Loading