Skip to content
29 changes: 29 additions & 0 deletions backend/audit/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,35 @@ class UploadReportForm(forms.Form):
schedule_prior_findings = forms.IntegerField(initial=0, required=False, min_value=1)
CAP_page = forms.IntegerField(initial=0, required=False, min_value=1)
upload_report = forms.FileField()
keep_previous_report = forms.BooleanField(required=False)

def clean(self):
"""
For 'Original' submissions, these fields do not really need cleaning - they're integers, so the default errors will do.
The view will handle erroneous report uploads.

For resubmissions, a user might upload an erroneous report and then indicate to keep their previous report.
Then, we want to clear all errors.
"""
cleaned_data = super().clean()
if cleaned_data.get("keep_previous_report"):
for field in [
"financial_statements",
"financial_statements_opinion",
"schedule_expenditures",
"schedule_expenditures_opinion",
"uniform_guidance_control",
"uniform_guidance_compliance",
"GAS_control",
"GAS_compliance",
"schedule_findings",
"schedule_prior_findings",
"CAP_page",
"upload_report",
]:
if field in self.errors:
del self.errors[field]
return cleaned_data


def _kvpair(info):
Expand Down
16 changes: 16 additions & 0 deletions backend/audit/templates/audit/upload-report.html
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,22 @@ <h2 class="usa-process-list__heading">{% if already_submitted %}Re-upload{% else
</p>
{% endif %}

{% if is_resubmission %}
<div class="usa-checkbox">
<input class="usa-checkbox__input"
id="keep-previous-report"
name="keep_previous_report"
type="checkbox" />
<label class="usa-checkbox__label" for="keep-previous-report">
My report did not change. Keep the old report for this resubmission.
</label>
</div>
{% endif %}

{% if form.non_field_errors %}
<span class="usa-error-message margin-top-2">{{ form.non_field_errors|striptags }}</span>
{% endif %}

<div hidden id="loader" class="cross-validation-loader margin-top-6">
<img src="{% static 'img/loader.svg' %}" alt="spinner for processing upload" />
</div>
Expand Down
Empty file.
31 changes: 28 additions & 3 deletions backend/audit/test_viewlib/test_resubmission_start_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from audit.models import SingleAuditChecklist
from audit.models.constants import STATUS
from dissemination.models import General

User = get_user_model()

Expand All @@ -16,20 +17,40 @@ class ResubmissionStartViewTests(TestCase):
invalid_report_id = "NOT-LONG-ENOUGH"
nonexistent_report_id = "LONGENOUGHBUTDOESNOTEXIST"
valid_report_id = "0123-01-SOURCE-0123456789"
valid_sibling_report_id = "3210-10-SOURCE-9876543210"
general_information = {
"auditee_uei": "auditee_uei",
"auditee_name": "auditee_name",
"auditee_fiscal_period_end": "2022-01-01",
}

# Recreated per test
def setUp(self):
"""Setup user and client."""
"""Setup prerequisite fake submissions, then add a user and client."""
self.valid_sac = baker.make(
SingleAuditChecklist,
report_id=self.valid_report_id,
submission_status=STATUS.DISSEMINATED,
general_information=self.general_information,
)
self.sibling_sac = baker.make(
SingleAuditChecklist,
report_id=self.valid_sibling_report_id,
submission_status=STATUS.DISSEMINATED,
general_information=self.general_information,
)
self.sibling_general = baker.make(
General,
report_id=self.valid_sibling_report_id,
audit_year="2022",
auditee_uei="auditee_uei",
)

self.user = baker.make(User)
self.client = Client()

def test_redirect_if_not_logged_in(self):
"""Test that accessing resubmission start page redirects if the user is not logged in"""
"""Test that accessing resubmission start page redirects if the user is not logged in."""
response = self.client.get(self.path_name)
self.assertAlmostEqual(response.status_code, 302)

Expand Down Expand Up @@ -62,4 +83,8 @@ def test_valid_report_id(self):
self.client.force_login(user=self.user)
response = self.client.post(self.path_name, {"report_id": self.valid_report_id})

self.assertRedirects(response, reverse("report_submission:eligibility"))
self.assertRedirects(
response,
reverse("report_submission:eligibility"),
fetch_redirect_response=False,
)
221 changes: 221 additions & 0 deletions backend/audit/test_viewlib/test_upload_report_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
from unittest.mock import patch

from audit.models import (
Access,
SingleAuditReportFile,
SubmissionEvent,
)
from audit.models.constants import SAC_SEQUENCE_ID
from audit.models.utils import get_next_sequence_id
from audit.test_views import (
_mock_gen_report_id,
_mock_login_and_scan,
_make_user_and_sac,
)
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
from model_bakery import baker

User = get_user_model()


class SingleAuditReportFileHandlerViewTests(TestCase):
valid_page_numbers = {
"financial_statements": 1,
"financial_statements_opinion": 2,
"schedule_expenditures": 3,
"schedule_expenditures_opinion": 4,
"uniform_guidance_control": 5,
"uniform_guidance_compliance": 6,
"GAS_control": 7,
"GAS_compliance": 8,
"schedule_findings": 9,
}

def test_login_required(self):
"""When an unauthenticated request is made."""

response = self.client.post(
reverse(
"audit:SingleAuditReport",
kwargs={"report_id": "12345"},
)
)

self.assertTemplateUsed(response, "home.html")
self.assertTrue(response.context["session_expired"])

def test_bad_report_id_returns_403(self):
"""When a request is made for a malformed or nonexistent report_id, a 403 error should be returned."""
user = baker.make(User)

self.client.force_login(user)

response = self.client.post(
reverse(
"audit:SingleAuditReport",
kwargs={
"report_id": "this is not a report id",
},
)
)

self.assertEqual(response.status_code, 403)

def test_inaccessible_audit_returns_403(self):
"""When a request is made for an audit that is inaccessible for this user, a 403 error should be returned."""
user, sac = _make_user_and_sac()

self.client.force_login(user)
response = self.client.post(
reverse(
"audit:SingleAuditReport",
kwargs={"report_id": sac.report_id},
)
)

self.assertEqual(response.status_code, 403)

def test_no_file_attached_returns_400(self):
"""When a request is made with no file attached, a 400 error should be returned."""
user, sac = _make_user_and_sac()
baker.make(Access, user=user, sac=sac)

self.client.force_login(user)

response = self.client.post(
reverse(
"audit:SingleAuditReport",
kwargs={"report_id": sac.report_id},
)
)

self.assertEqual(response.status_code, 400)

@patch("audit.validators._scan_file")
def test_valid_file_upload(self, mock_scan_file):
"""Test that uploading a valid SAR update the SAC accordingly."""
sequence = get_next_sequence_id(SAC_SEQUENCE_ID)
sac = _mock_login_and_scan(
self.client,
mock_scan_file,
id=sequence,
report_id=_mock_gen_report_id(sequence),
)

with open("audit/fixtures/basic.pdf", "rb") as pdf_file:
response = self.client.post(
reverse(
"audit:SingleAuditReport",
kwargs={
"report_id": sac.report_id,
},
),
data={"FILES": pdf_file},
)

self.assertEqual(response.status_code, 302)

submission_events = SubmissionEvent.objects.filter(sac=sac)

# the most recent event should be AUDIT_REPORT_PDF_UPDATED
event_count = len(submission_events)
self.assertGreaterEqual(event_count, 1)
self.assertEqual(
submission_events[event_count - 1].event,
SubmissionEvent.EventType.AUDIT_REPORT_PDF_UPDATED,
)

@patch("audit.validators._scan_file")
def test_new_report_upload_via_upload_report_view(self, mock_scan_file):
"""
When a user uploads a new report, `keep_previous_report` is either False or absent.
Ensure the view redirects the user. Then pull down the SAR and spot check the data and file.
"""
sequence = get_next_sequence_id(SAC_SEQUENCE_ID)
sac = _mock_login_and_scan(
self.client,
mock_scan_file,
id=sequence,
report_id=_mock_gen_report_id(sequence),
)

with open("audit/fixtures/basic.pdf", "rb") as pdf_file:
response = self.client.post(
reverse(
"audit:UploadReport",
kwargs={"report_id": sac.report_id},
),
data={**self.valid_page_numbers, "upload_report": pdf_file},
)

self.assertEqual(response.status_code, 302)

sar = SingleAuditReportFile.objects.filter(sac=sac).latest("date_created")
self.assertIsNotNone(sar)
self.assertEqual(
sar.component_page_numbers["schedule_findings"],
self.valid_page_numbers["schedule_findings"],
)

@patch("audit.views.upload_report_view.copy_file")
@patch("audit.validators._scan_file")
def test_keep_previous_report_copies_data(self, mock_scan_file, mock_copy_file):
"""
When a user opts to copy their report during a resubmission, `keep_previous_report` is either True.
Create a previous report. Ensure the view redirects the user. Ensure `copy_file` was run with the right params. Spot check the data and file.
"""
# Create the necessary "previous" report data.
prev_sequence = get_next_sequence_id(SAC_SEQUENCE_ID)
prev_sac = _mock_login_and_scan(
self.client,
mock_scan_file,
id=prev_sequence,
report_id=_mock_gen_report_id(prev_sequence),
)

baker.make(
SingleAuditReportFile,
sac=prev_sac,
file=f"singleauditreport/{prev_sac.report_id}.pdf",
filename=f"{prev_sac.report_id}.pdf",
component_page_numbers=self.valid_page_numbers,
)

# Create the necessary "current" report data, link it to the "previous".
current_sequence = get_next_sequence_id(SAC_SEQUENCE_ID)
current_sac = _mock_login_and_scan(
self.client,
mock_scan_file,
id=current_sequence,
report_id=_mock_gen_report_id(current_sequence),
resubmission_meta={"previous_report_id": prev_sac.report_id},
)

# POST with keep_previous_report checked
response = self.client.post(
reverse(
"audit:UploadReport",
kwargs={"report_id": current_sac.report_id},
),
data={"keep_previous_report": True},
)

self.assertEqual(response.status_code, 302)

# Ensure the copy function was called.
mock_copy_file.assert_called_once_with(
f"singleauditreport/{prev_sac.report_id}.pdf",
f"singleauditreport/{current_sac.report_id}.pdf",
)

# Spot check the "new" file.
new_sar = SingleAuditReportFile.objects.filter(sac=current_sac).latest(
"date_created"
)
self.assertIsNotNone(new_sar)
self.assertEqual(
new_sar.component_page_numbers["schedule_findings"],
self.valid_page_numbers["schedule_findings"],
)
Loading
Loading