Skip to content

Commit

Permalink
Rerunning individual validations before cross validations and submiss…
Browse files Browse the repository at this point in the history
…ion (#4203)

* Running wb validations in pre-submission validation

* Displaying generic error and moving to separate method

* Lint

* Lint

* Lint

* Returning empty object when no errors

* Running wb validations during submission

* Lint

* Lint

* Fixing test_post_redirect

* Fixing test_ready_for_certification

* Fixing test_submission

* Fixing test_unlock_after_certification

* Lint

* Typo
  • Loading branch information
phildominguez-gsa authored Aug 21, 2024
1 parent 9d4f0a2 commit e1605f2
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 81 deletions.
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

0 comments on commit e1605f2

Please sign in to comment.