diff --git a/backend/audit/admin.py b/backend/audit/admin.py index a5a27c874..8adf802e5 100644 --- a/backend/audit/admin.py +++ b/backend/audit/admin.py @@ -13,7 +13,11 @@ UeiValidationWaiver, ) from audit.models.models import STATUS -from audit.models.viewflow import sac_transition +from audit.models.viewflow import ( + sac_flag_for_removal, + sac_revert_from_flagged_for_removal, + sac_transition, +) from audit.validators import ( validate_auditee_certification_json, validate_auditor_certification_json, @@ -24,6 +28,76 @@ logger = logging.getLogger(__name__) +@admin.action(description="Revert selected report(s) to In Progress") +def revert_to_in_progress(modeladmin, request, queryset): + successful_reverts = [] + errors = [] + + for sac in queryset: + if sac.submission_status == STATUS.FLAGGED_FOR_REMOVAL: + try: + sac_revert_from_flagged_for_removal(sac, request.user) + sac.save() + successful_reverts.append(sac.report_id) + except Exception as e: + modeladmin.message_user( + request, + f"Error reverting {sac.report_id}: {str(e)}", + level=messages.ERROR, + ) + errors.append(sac.report_id) + else: + modeladmin.message_user( + request, + f"Report {sac.report_id} is not flagged for removal.", + level=messages.WARNING, + ) + errors.append(sac.report_id) + + if successful_reverts: + modeladmin.message_user( + request, + f"Successfully reverted report(s) ({', '.join(successful_reverts)}) back to In Progress.", + level=messages.SUCCESS, + ) + + if errors: + modeladmin.message_user( + request, + f"Unable to revert report(s) ({', '.join(errors)}) back to In Progress.", + level=messages.ERROR, + ) + + +@admin.action(description="Flag selected report(s) for removal") +def flag_for_removal(modeladmin, request, queryset): + + flagged = [] + already_flagged = [] + + for sac in queryset: + if sac.submission_status != STATUS.FLAGGED_FOR_REMOVAL: + sac_flag_for_removal(sac, request.user) + sac.save() + flagged.append(sac.report_id) + else: + already_flagged.append(sac.report_id) + + if flagged: + modeladmin.message_user( + request, + f"Successfully flagged report(s) ({', '.join(flagged)}) for removal.", + level=messages.SUCCESS, + ) + + if already_flagged: + modeladmin.message_user( + request, + f"Report(s) ({', '.join(already_flagged)}) were already flagged.", + level=messages.WARNING, + ) + + class SACAdmin(admin.ModelAdmin): """ Support for read-only staff access, and control of what fields are present and @@ -41,6 +115,7 @@ def has_view_permission(self, request, obj=None): "report_id", "cognizant_agency", "oversight_agency", + "submission_status", ) list_filter = [ "cognizant_agency", @@ -50,6 +125,7 @@ def has_view_permission(self, request, obj=None): ] readonly_fields = ("submitted_by",) search_fields = ("general_information__auditee_uei", "report_id") + actions = [revert_to_in_progress, flag_for_removal] class AccessAdmin(admin.ModelAdmin): 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/manage-submission.html b/backend/audit/templates/audit/manage-submission.html index 75f988348..dc143a50a 100644 --- a/backend/audit/templates/audit/manage-submission.html +++ b/backend/audit/templates/audit/manage-submission.html @@ -85,6 +85,8 @@
Are you sure you want to remove the following report?
+ +