diff --git a/backend/audit/migrations/0015_alter_singleauditchecklist_submission_status_and_more.py b/backend/audit/migrations/0015_alter_singleauditchecklist_submission_status_and_more.py new file mode 100644 index 000000000..0b473f184 --- /dev/null +++ b/backend/audit/migrations/0015_alter_singleauditchecklist_submission_status_and_more.py @@ -0,0 +1,125 @@ +# Generated by Django 5.1.2 on 2024-12-17 16:56 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("audit", "0014_alter_sacvalidationwaiver_waiver_types"), + ] + + operations = [ + migrations.AlterField( + model_name="singleauditchecklist", + name="submission_status", + field=models.CharField( + choices=[ + ("in_progress", "In Progress"), + ("flagged_for_removal", "Flagged for Removal"), + ("ready_for_certification", "Ready for Certification"), + ("auditor_certified", "Auditor Certified"), + ("auditee_certified", "Auditee Certified"), + ("certified", "Certified"), + ("submitted", "Submitted"), + ("disseminated", "Disseminated"), + ], + default="in_progress", + ), + ), + migrations.AlterField( + model_name="singleauditchecklist", + name="transition_name", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField( + choices=[ + ("in_progress", "In Progress"), + ("flagged_for_removal", "Flagged for Removal"), + ("ready_for_certification", "Ready for Certification"), + ("auditor_certified", "Auditor Certified"), + ("auditee_certified", "Auditee Certified"), + ("certified", "Certified"), + ("submitted", "Submitted"), + ("disseminated", "Disseminated"), + ], + max_length=40, + ), + blank=True, + default=list, + null=True, + size=None, + ), + ), + migrations.AlterField( + model_name="submissionevent", + name="event", + field=models.CharField( + choices=[ + ("access-granted", "Access granted"), + ("additional-eins-updated", "Additional EINs updated"), + ("additional-eins-deleted", "Additional EINs deleted"), + ("additional-ueis-updated", "Additional UEIs updated"), + ("additional-ueis-deleted", "Additional UEIs deleted"), + ("audit-information-updated", "Audit information updated"), + ("audit-report-pdf-updated", "Audit report PDF updated"), + ( + "auditee-certification-completed", + "Auditee certification completed", + ), + ( + "auditor-certification-completed", + "Auditor certification completed", + ), + ( + "corrective-action-plan-updated", + "Corrective action plan updated", + ), + ( + "corrective-action-plan-deleted", + "Corrective action plan deleted", + ), + ("created", "Created"), + ("federal-awards-updated", "Federal awards updated"), + ( + "federal-awards-audit-findings-updated", + "Federal awards audit findings updated", + ), + ( + "federal-awards-audit-findings-deleted", + "Federal awards audit findings deleted", + ), + ( + "federal-awards-audit-findings-text-updated", + "Federal awards audit findings text updated", + ), + ( + "federal-awards-audit-findings-text-deleted", + "Federal awards audit findings text deleted", + ), + ( + "findings-uniform-guidance-updated", + "Findings uniform guidance updated", + ), + ( + "findings-uniform-guidance-deleted", + "Findings uniform guidance deleted", + ), + ("general-information-updated", "General information updated"), + ("locked-for-certification", "Locked for certification"), + ("unlocked-after-certification", "Unlocked after certification"), + ("notes-to-sefa-updated", "Notes to SEFA updated"), + ("secondary-auditors-updated", "Secondary auditors updated"), + ("secondary-auditors-deleted", "Secondary auditors deleted"), + ("submitted", "Submitted to the FAC for processing"), + ("disseminated", "Copied to dissemination tables"), + ("tribal-consent-updated", "Tribal audit consent updated"), + ( + "flagged-submission-for-removal", + "Flagged submission for removal", + ), + ("cancel-removal-flag", "Cancel removal flag"), + ] + ), + ), + ] diff --git a/backend/audit/models/models.py b/backend/audit/models/models.py index e35b1d06e..252d1a3f2 100644 --- a/backend/audit/models/models.py +++ b/backend/audit/models/models.py @@ -171,6 +171,7 @@ class STATUS: CERTIFIED = "certified" SUBMITTED = "submitted" DISSEMINATED = "disseminated" + FLAGGED_FOR_REMOVAL = "flagged_for_removal" class SingleAuditChecklist(models.Model, GeneralInformationMixin): # type: ignore @@ -303,6 +304,7 @@ def get_statuses(self) -> type[STATUS]: # Constants: STATUS_CHOICES = ( (STATUS.IN_PROGRESS, "In Progress"), + (STATUS.FLAGGED_FOR_REMOVAL, "Flagged for Removal"), (STATUS.READY_FOR_CERTIFICATION, "Ready for Certification"), (STATUS.AUDITOR_CERTIFIED, "Auditor Certified"), (STATUS.AUDITEE_CERTIFIED, "Auditee Certified"), diff --git a/backend/audit/models/submission_event.py b/backend/audit/models/submission_event.py index d4d675bb0..7db884718 100644 --- a/backend/audit/models/submission_event.py +++ b/backend/audit/models/submission_event.py @@ -42,6 +42,8 @@ class EventType: SUBMITTED = "submitted" DISSEMINATED = "disseminated" TRIBAL_CONSENT_UPDATED = "tribal-consent-updated" + FLAGGED_SUBMISSION_FOR_REMOVAL = "flagged-submission-for-removal" + CANCEL_REMOVAL_FLAG = "cancel-removal-flag" EVENT_TYPES = ( (EventType.ACCESS_GRANTED, _("Access granted")), @@ -96,6 +98,11 @@ class EventType: (EventType.SUBMITTED, _("Submitted to the FAC for processing")), (EventType.DISSEMINATED, _("Copied to dissemination tables")), (EventType.TRIBAL_CONSENT_UPDATED, _("Tribal audit consent updated")), + ( + EventType.FLAGGED_SUBMISSION_FOR_REMOVAL, + _("Flagged submission for removal"), + ), + (EventType.CANCEL_REMOVAL_FLAG, _("Cancel removal flag")), ) sac = models.ForeignKey("audit.SingleAuditChecklist", on_delete=models.CASCADE) diff --git a/backend/audit/models/viewflow.py b/backend/audit/models/viewflow.py index 35e460f0a..1d12515b3 100644 --- a/backend/audit/models/viewflow.py +++ b/backend/audit/models/viewflow.py @@ -29,6 +29,41 @@ def sac_revert_from_submitted(sac): return False +def sac_revert_from_flagged_for_removal(sac, user): + """ + Transitions the submission_state for a SingleAuditChecklist back + to "in_progress" so the user can continue working on it. + This should be accessible to django admin. + """ + if sac.submission_status == STATUS.FLAGGED_FOR_REMOVAL: + flow = SingleAuditChecklistFlow(sac) + + flow.transition_to_in_progress_again() + + with CurationTracking(): + sac.save( + event_user=user, + event_type=SubmissionEvent.EventType.CANCEL_REMOVAL_FLAG, + ) + + +def sac_flag_for_removal(sac, user): + """ + Transitions the submission_state for a SingleAuditChecklist to "flagged_for_removal". + This should be accessible to django admin. + """ + if sac.submission_status == STATUS.IN_PROGRESS: + flow = SingleAuditChecklistFlow(sac) + + flow.transition_to_flagged_for_removal() + + with CurationTracking(): + sac.save( + event_user=user, + event_type=SubmissionEvent.EventType.FLAGGED_SUBMISSION_FOR_REMOVAL, + ) + + def sac_transition(request, sac, **kwargs): """ Transitions the submission_state for a SingleAuditChecklist (sac). @@ -54,6 +89,14 @@ def sac_transition(request, sac, **kwargs): ) return True + elif target == STATUS.FLAGGED_FOR_REMOVAL: + flow.transition_to_flagged_for_removal() + sac.save( + event_user=user, + event_type=SubmissionEvent.EventType.FLAGGED_SUBMISSION_FOR_REMOVAL, + ) + return True + elif target == STATUS.READY_FOR_CERTIFICATION: flow.transition_to_ready_for_certification() sac.save( @@ -127,11 +170,24 @@ def transition_to_ready_for_certification(self): self.sac.transition_name.append(STATUS.READY_FOR_CERTIFICATION) self.sac.transition_date.append(datetime.datetime.now(datetime.timezone.utc)) + @state.transition( + source=STATUS.IN_PROGRESS, + target=STATUS.FLAGGED_FOR_REMOVAL, + ) + def transition_to_flagged_for_removal(self): + """ + The permission checks verifying that the user attempting to do this has + the appropriate privileges will be done at the view level. + """ + self.sac.transition_name.append(STATUS.FLAGGED_FOR_REMOVAL) + self.sac.transition_date.append(datetime.datetime.now(datetime.timezone.utc)) + @state.transition( source=[ STATUS.READY_FOR_CERTIFICATION, STATUS.AUDITOR_CERTIFIED, STATUS.AUDITEE_CERTIFIED, + STATUS.FLAGGED_FOR_REMOVAL, ], target=STATUS.IN_PROGRESS, ) diff --git a/backend/audit/templates/audit/my_submissions.html b/backend/audit/templates/audit/my_submissions.html index b570b99fd..1ba43e74d 100644 --- a/backend/audit/templates/audit/my_submissions.html +++ b/backend/audit/templates/audit/my_submissions.html @@ -27,6 +27,8 @@

Audits in progress

data-open-modal>UEI Fiscal period end date + User Access + Delete @@ -46,6 +48,20 @@

Audits in progress

{{ item.report_id }} {{ item.auditee_uei }} {{ item.fiscal_year_end_date }} + + + + + + + + + + {% endfor %} diff --git a/backend/audit/templates/audit/remove-submission-in-progress.html b/backend/audit/templates/audit/remove-submission-in-progress.html new file mode 100644 index 000000000..ef32f58e5 --- /dev/null +++ b/backend/audit/templates/audit/remove-submission-in-progress.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} +{% load static %} +{% load sprite_helper %} + +{% block content %} +
+

Confirm Report Removal

+

Are you sure you want to remove the following report?

+ +
+
+
    +
  • Auditee UEI: {{ auditee_uei }}
  • +
  • Auditee Name: {{ auditee_name }}
  • +
  • Report ID: {{ report_id }}
  • +
  • Fiscal Period End Date: {{ fiscal_year_end_date }}
  • +
+
+
+ +
+ {% csrf_token %} +
+ + Return to Submissions +
+
+
+{% endblock content %} diff --git a/backend/audit/urls.py b/backend/audit/urls.py index 88e7b610a..d8c6260aa 100644 --- a/backend/audit/urls.py +++ b/backend/audit/urls.py @@ -110,6 +110,11 @@ def camel_to_hyphen(raw: str) -> str: views.RemoveEditorView.as_view(), name="RemoveEditorView", ), + path( + "manage-submission/remove-report/", + views.RemoveSubmissionView.as_view(), + name="RemoveSubmissionInProgress", + ), path( "workbook/xlsx//", views.PredisseminationXlsxDownloadView.as_view(), diff --git a/backend/audit/views/__init__.py b/backend/audit/views/__init__.py index a9a839264..bc3fe9c17 100644 --- a/backend/audit/views/__init__.py +++ b/backend/audit/views/__init__.py @@ -14,6 +14,7 @@ PredisseminationPdfDownloadView, PredisseminationSummaryReportDownloadView, ) +from .remove_submission_in_progress import RemoveSubmissionView from .submission_progress_view import ( # noqa SubmissionProgressView, submission_progress_check, @@ -67,4 +68,5 @@ TribalDataConsent, UnlockAfterCertificationView, UploadReportView, + RemoveSubmissionView, ] diff --git a/backend/audit/views/remove_submission_in_progress.py b/backend/audit/views/remove_submission_in_progress.py new file mode 100644 index 000000000..f0c94748d --- /dev/null +++ b/backend/audit/views/remove_submission_in_progress.py @@ -0,0 +1,64 @@ +import logging +from django.shortcuts import redirect, render, reverse +from django.views import generic +from django.core.exceptions import PermissionDenied + +from audit.mixins import ( + SingleAuditChecklistAccessRequiredMixin, +) +from audit.models import ( + Access, + SingleAuditChecklist, +) +from audit.views.views import verify_status +from audit.models.models import STATUS +from audit.models.viewflow import SingleAuditChecklistFlow + +logger = logging.getLogger(__name__) + + +class RemoveSubmissionView(SingleAuditChecklistAccessRequiredMixin, generic.View): + """ + View for removing an audit. + """ + + template = "audit/remove-submission-in-progress.html" + + @verify_status(STATUS.IN_PROGRESS) + def get(self, request, *args, **kwargs): + """ + Show the audit to be removed and confirmation form. + """ + report_id = kwargs["report_id"] + sac = SingleAuditChecklist.objects.get(report_id=report_id) + + if not Access.objects.filter( + email=request.user.email, sac=sac, role="editor" + ).exists(): + raise PermissionDenied("Only authorized auditors can remove audits.") + + context = { + "auditee_uei": sac.general_information["auditee_uei"], + "auditee_name": sac.general_information.get("auditee_name"), + "report_id": sac.report_id, + "fiscal_year_end_date": sac.general_information.get( + "auditee_fiscal_period_end" + ), + } + + return render(request, self.template, context) + + @verify_status(STATUS.IN_PROGRESS) + def post(self, request, *args, **kwargs): + """ + Remove the audit and redirect to the audits list. + """ + report_id = kwargs["report_id"] + sac = SingleAuditChecklist.objects.get(report_id=report_id) + + flow = SingleAuditChecklistFlow(sac) + flow.transition_to_flagged_for_removal() + sac.save() + url = reverse("audit:MySubmissions") + + return redirect(url)