diff --git a/backend/audit/forms.py b/backend/audit/forms.py
index bfdd6073f4..5967f5feac 100644
--- a/backend/audit/forms.py
+++ b/backend/audit/forms.py
@@ -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):
diff --git a/backend/audit/templates/audit/upload-report.html b/backend/audit/templates/audit/upload-report.html
index 6d7e11d50e..843ed3540b 100644
--- a/backend/audit/templates/audit/upload-report.html
+++ b/backend/audit/templates/audit/upload-report.html
@@ -96,6 +96,22 @@
{% if already_submitted %}Re-upload{% else
{% endif %}
+ {% if is_resubmission %}
+
+
+
+
+ {% endif %}
+
+ {% if form.non_field_errors %}
+ {{ form.non_field_errors|striptags }}
+ {% endif %}
+
diff --git a/backend/audit/test_viewlib/__init__.py b/backend/audit/test_viewlib/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/backend/audit/test_viewlib/test_resubmission_start_view.py b/backend/audit/test_viewlib/test_resubmission_start_view.py
index f331e19792..0cd3ac1843 100644
--- a/backend/audit/test_viewlib/test_resubmission_start_view.py
+++ b/backend/audit/test_viewlib/test_resubmission_start_view.py
@@ -6,6 +6,7 @@
from audit.models import SingleAuditChecklist
from audit.models.constants import STATUS
+from dissemination.models import General
User = get_user_model()
@@ -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)
@@ -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,
+ )
diff --git a/backend/audit/test_viewlib/test_upload_report_view.py b/backend/audit/test_viewlib/test_upload_report_view.py
new file mode 100644
index 0000000000..b89d86dc41
--- /dev/null
+++ b/backend/audit/test_viewlib/test_upload_report_view.py
@@ -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"],
+ )
diff --git a/backend/audit/test_views.py b/backend/audit/test_views.py
index 9760388e9c..5bea764380 100644
--- a/backend/audit/test_views.py
+++ b/backend/audit/test_views.py
@@ -1624,102 +1624,6 @@ def test_excel_file_not_saved_on_validation_failure(self):
)
self.assertFalse(ExcelFile.objects.exists())
-
-class SingleAuditReportFileHandlerViewTests(TestCase):
- 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_valid_file_upload_for_additional_ueis(self, mock_scan_file):
"""When a valid Excel file is uploaded, the file should be stored and the SingleAuditChecklist should be updated to include the uploaded Additional UEIs data"""
diff --git a/backend/audit/views/upload_report_view.py b/backend/audit/views/upload_report_view.py
index 5e29276f23..8dd38adb2e 100644
--- a/backend/audit/views/upload_report_view.py
+++ b/backend/audit/views/upload_report_view.py
@@ -1,10 +1,13 @@
import logging
from django.core.exceptions import BadRequest, PermissionDenied, ValidationError
+from django.core.files.uploadedfile import UploadedFile
+from django.http import HttpRequest, HttpResponse
from django.shortcuts import render, redirect
from django.views import generic
from django.urls import reverse
+from audit.forms import UploadReportForm
from audit.mixins import (
SingleAuditChecklistAccessRequiredMixin,
)
@@ -14,8 +17,8 @@
SingleAuditReportFile,
Audit,
)
-from audit.forms import UploadReportForm
from audit.models.constants import EventType
+from dissemination.file_downloads import copy_file
logging.basicConfig(
format="%(asctime)s %(levelname)-8s %(module)s:%(lineno)d %(message)s"
@@ -39,7 +42,7 @@ def __init__(self, text="", id="", required=True, hint=None):
class UploadReportView(SingleAuditChecklistAccessRequiredMixin, generic.View):
- def page_number_inputs(self):
+ def page_number_inputs(self) -> list[PageInput]:
"""
Build the input elements to be passed to the context for use in
audit/templates/audit/upload-report.html
@@ -97,11 +100,16 @@ def get(self, request, *args, **kwargs):
sar = SingleAuditReportFile.objects.filter(sac_id=sac.id)
if sar.exists():
sar = sar.latest("date_created")
-
current_info = {
"cleaned_data": getattr(sar, "component_page_numbers", {}),
}
+ previous_report_id = (
+ sac.resubmission_meta.get("previous_report_id")
+ if sac.resubmission_meta
+ else None
+ )
+
context = {
"auditee_name": sac.auditee_name,
"report_id": report_id,
@@ -109,6 +117,7 @@ def get(self, request, *args, **kwargs):
"user_provided_organization_type": sac.user_provided_organization_type,
"page_number_inputs": self.page_number_inputs(),
"already_submitted": True if sar else False,
+ "is_resubmission": True if previous_report_id else False,
"form": current_info,
}
@@ -125,11 +134,14 @@ def post(self, request, *args, **kwargs):
try:
# TODO SOT: Switch to `audit`
sac = SingleAuditChecklist.objects.get(report_id=report_id)
- # In the transition (SOT1.5), the audit MUST exist at this point,
- # becuase it was created with the SAC. So, we should at least get *something*.
- audit = Audit.objects.find_audit_or_none(report_id)
form = UploadReportForm(request.POST, request.FILES)
+ previous_report_id = (
+ sac.resubmission_meta.get("previous_report_id")
+ if sac.resubmission_meta
+ else None
+ )
+
# Standard context always needed on this page
context = {
"auditee_name": sac.auditee_name,
@@ -137,59 +149,103 @@ def post(self, request, *args, **kwargs):
"auditee_uei": sac.auditee_uei,
"user_provided_organization_type": sac.user_provided_organization_type,
"page_number_inputs": self.page_number_inputs(),
+ "is_resubmission": bool(previous_report_id),
}
- if form.is_valid():
- file = request.FILES["upload_report"]
- # SOT TODO: The audit.id, per comment above, MUST exist at this point.
- # Pass the audit ID if we have one. Otherwise, None is valid. Revert to just `audit.id` after TODO.
- sar_file = self.reformat_form_data(
- file, form, sac.id, audit.id if audit else None
+ # Find form errors and return if any exist, then EITHER:
+ # 1. For resubmissions that opt in, copy the previous report.
+ # 2. For original or updated resubmissions, validate and store as normal.
+ if not form.is_valid():
+ return render(
+ request, "audit/upload-report.html", context | {"form": form}
)
- # Try to save the formatted form data. If it fails on the file
- # (encryption issues, file size issues), add and pass back the file errors.
- # If it fails due to something else, re-raise it to be handled further below.
- try:
- sar_file.full_clean()
- sar_file.save(
- event_user=request.user,
- event_type=EventType.AUDIT_REPORT_PDF_UPDATED,
- )
-
- self._save_audit(
- report_id=report_id, sar_file=sar_file, request=request
- )
- except ValidationError as err:
- for issue in err.error_dict.get("file"):
- form.add_error("upload_report", issue)
- return render(
- request, "audit/upload-report.html", context | {"form": form}
- )
- except Exception as err:
- raise err
-
- # Form data saved, redirect to checklist.
- return redirect(reverse("audit:SubmissionProgress", args=[report_id]))
-
- # form.is_valid() failed (standard Django issues). Show the errors.
- return render(request, "audit/upload-report.html", context | {"form": form})
+ if form.cleaned_data.get("keep_previous_report") and previous_report_id:
+ return self._handle_keep_previous_report(
+ request, report_id, previous_report_id, form, context
+ )
+
+ return self._handle_new_report(request, report_id, form, context)
except SingleAuditChecklist.DoesNotExist as err:
raise PermissionDenied("You do not have access to this audit.") from err
except LateChangeError:
return render(request, "audit/no-late-changes.html")
-
except Exception as err:
logger.error("Unexpected error in UploadReportView post:\n %s", err)
raise BadRequest() from err
- def reformat_form_data(self, file, form, sac_id, audit_id):
+ def _handle_keep_previous_report(
+ self,
+ request: HttpRequest,
+ report_id: str,
+ previous_report_id: str,
+ form: UploadReportForm,
+ context: dict,
+ ) -> HttpResponse:
+ """
+ Copy the previous submission's SingleAuditReportFile and PDF to the current resubmission.
+ """
+ sac = SingleAuditChecklist.objects.get(report_id=report_id)
+ audit = Audit.objects.find_audit_or_none(report_id)
+
+ try:
+ self.copy_previous_report_data(
+ previous_report_id=previous_report_id,
+ current_sac=sac,
+ current_audit=audit,
+ request=request,
+ )
+ except Exception as err:
+ logger.error("Unexpected error copying a SingleAuditReportFile: {err}")
+ form.add_error(None, f"Unable to copy the previous report: {err}")
+ return render(request, "audit/upload-report.html", context | {"form": form})
+
+ return redirect(reverse("audit:SubmissionProgress", args=[report_id]))
+
+ def _handle_new_report(
+ self,
+ request: HttpRequest,
+ report_id: str,
+ form: UploadReportForm,
+ context: dict,
+ ) -> HttpResponse:
+ """
+ Validate and store a newly uploaded report.
+ """
+ sac = SingleAuditChecklist.objects.get(report_id=report_id)
+ audit = Audit.objects.find_audit_or_none(report_id)
+
+ file = request.FILES["upload_report"]
+ sar_file = self.reformat_form_data(
+ file, form, sac.id, audit.id if audit else None
+ )
+
+ try:
+ sar_file.full_clean()
+ sar_file.save(
+ event_user=request.user,
+ event_type=EventType.AUDIT_REPORT_PDF_UPDATED,
+ )
+ self._save_audit(report_id=report_id, sar_file=sar_file, request=request)
+ except ValidationError as err:
+ for issue in err.error_dict.get("file"):
+ form.add_error("upload_report", issue)
+ return render(request, "audit/upload-report.html", context | {"form": form})
+
+ return redirect(reverse("audit:SubmissionProgress", args=[report_id]))
+
+ def reformat_form_data(
+ self,
+ file: UploadedFile,
+ form: UploadReportForm,
+ sac_id: int,
+ audit_id: int | None,
+ ) -> SingleAuditReportFile:
"""
Given the file, form, and report_id, return the formatted SingleAuditReportFile.
Maps cleaned form data into an object to be passed alongside the file, filename, and report id.
"""
-
component_page_numbers = {
"financial_statements": form.cleaned_data["financial_statements"],
"financial_statements_opinion": form.cleaned_data[
@@ -222,8 +278,52 @@ def reformat_form_data(self, file, form, sac_id, audit_id):
)
return sar_file
+ def copy_previous_report_data(
+ self,
+ previous_report_id: str,
+ current_sac: SingleAuditChecklist,
+ current_audit: Audit | None,
+ request: HttpRequest,
+ ) -> None:
+ """
+ Copy the SingleAuditReportFile and the associated s3 object from the
+ previous submission to the current resubmission.
+ """
+ previous_sac = SingleAuditChecklist.objects.get(report_id=previous_report_id)
+ previous_sar = SingleAuditReportFile.objects.filter(sac=previous_sac).latest(
+ "date_created"
+ )
+
+ # Copy the S3 object
+ source_key = f"singleauditreport/{previous_sac.report_id}.pdf"
+ dest_key = f"singleauditreport/{current_sac.report_id}.pdf"
+ copy_file(source_key, dest_key)
+
+ # Copy the SingleAuditReportFile row
+ new_sar = SingleAuditReportFile(
+ file=dest_key,
+ filename=f"{current_sac.report_id}.pdf",
+ sac=current_sac,
+ audit=current_audit,
+ component_page_numbers=previous_sar.component_page_numbers,
+ )
+ new_sar.save(
+ event_user=request.user,
+ event_type=EventType.AUDIT_REPORT_PDF_UPDATED,
+ )
+
+ # Mirror onto the Audit model if it exists
+ if current_audit:
+ self._save_audit(
+ report_id=current_sac.report_id, sar_file=new_sar, request=request
+ )
+
@staticmethod
- def _save_audit(report_id, sar_file, request):
+ def _save_audit(
+ report_id: str,
+ sar_file: SingleAuditReportFile,
+ request: HttpRequest,
+ ) -> None:
# TODO: Update Post SOC Launch : Delete and move done for linting complexity
audit = Audit.objects.find_audit_or_none(report_id=report_id)
if audit:
diff --git a/backend/dissemination/file_downloads.py b/backend/dissemination/file_downloads.py
index 9f2c65028e..d8d33c7c0c 100644
--- a/backend/dissemination/file_downloads.py
+++ b/backend/dissemination/file_downloads.py
@@ -91,7 +91,7 @@ def file_exists(filename, show_warning=True):
return True
except ClientError:
if show_warning:
- logger.warn(f"Unable to locate file {filename} in S3!")
+ logger.warning(f"Unable to locate file {filename} in S3!")
return False
@@ -125,3 +125,33 @@ def get_download_url(filename):
raise Http404("File not found")
except ClientError:
raise Http404("File not found")
+
+
+def copy_file(source_key, dest_key):
+ """
+ Copy a file within the private S3 bucket from source_key to dest_key.
+ Returns True on success, raises Http404 if the source does not exist.
+ """
+ if not file_exists(source_key):
+ raise Http404(f"Unable to locate file {source_key} in S3!")
+
+ s3_client = boto3_client(
+ service_name="s3",
+ region_name=settings.AWS_S3_PRIVATE_REGION_NAME,
+ aws_access_key_id=settings.AWS_PRIVATE_ACCESS_KEY_ID,
+ aws_secret_access_key=settings.AWS_PRIVATE_SECRET_ACCESS_KEY,
+ endpoint_url=settings.AWS_S3_PRIVATE_INTERNAL_ENDPOINT,
+ config=Config(signature_version="s3v4"),
+ )
+
+ source_bucket = settings.AWS_PRIVATE_STORAGE_BUCKET_NAME
+ try:
+ s3_client.copy_object(
+ Bucket=source_bucket,
+ Key=dest_key,
+ CopySource={"Bucket": source_bucket, "Key": source_key},
+ )
+ return True
+ except ClientError as err:
+ logger.error(f"Failed to copy S3 object {source_key} -> {dest_key}: {err}")
+ raise Http404("Failed to copy file") from err
diff --git a/backend/dissemination/test_file_downloads.py b/backend/dissemination/test_file_downloads.py
index 419dab9654..9451f99736 100644
--- a/backend/dissemination/test_file_downloads.py
+++ b/backend/dissemination/test_file_downloads.py
@@ -1,4 +1,4 @@
-from datetime import datetime
+from datetime import datetime, timezone
from django.conf import settings
from django.http import Http404
@@ -23,7 +23,7 @@ def setUp(self):
pass
def _report_id(self, sequence, source):
- today = datetime.utcnow().date().isoformat()
+ today = datetime.now(timezone.utc).date().isoformat()
return generate_sac_report_id(sequence=sequence, end_date=today, source=source)
def test_gsafac_no_singleauditchecklist(self):
diff --git a/backend/static/js/upload-report.js b/backend/static/js/upload-report.js
index 1575485075..fb6dbbac73 100644
--- a/backend/static/js/upload-report.js
+++ b/backend/static/js/upload-report.js
@@ -1,20 +1,48 @@
var FORM = document.getElementById('upload-report__form');
-const continue_button = document.getElementById(`continue`); //