diff --git a/backend/audit/admin.py b/backend/audit/admin.py index a5a27c874..5e84c4ae8 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/test_admin.py b/backend/audit/test_admin.py index 10c3f4b78..be6e22edd 100644 --- a/backend/audit/test_admin.py +++ b/backend/audit/test_admin.py @@ -4,7 +4,12 @@ from django.contrib.messages.storage.fallback import FallbackStorage from .models import SacValidationWaiver, SingleAuditChecklist from .models.models import STATUS -from .admin import SacValidationWaiverAdmin +from .admin import ( + SACAdmin, + SacValidationWaiverAdmin, + flag_for_removal, + revert_to_in_progress, +) from django.utils import timezone from model_bakery import baker from django.contrib.sessions.middleware import SessionMiddleware @@ -161,3 +166,63 @@ def test_handle_auditee_certification(self): # Checking results self.sac.refresh_from_db() self.assertEqual(self.sac.submission_status, STATUS.AUDITEE_CERTIFIED) + + +class TestAdminActions(TestCase): + def setUp(self): + self.site = AdminSite() + self.admin = SACAdmin(SingleAuditChecklist, self.site) + self.request = RequestFactory().get("/admin/audit/singleauditchecklist/") + self.request.user = baker.make(User, is_staff=True) + self.request.session = {} + self.request._messages = FallbackStorage(self.request) + + # Sample records + self.report1 = baker.make( + SingleAuditChecklist, + report_id="RPT001", + submission_status=STATUS.FLAGGED_FOR_REMOVAL, + ) + self.report2 = baker.make( + SingleAuditChecklist, + report_id="RPT002", + submission_status=STATUS.IN_PROGRESS, + ) + + def test_revert_to_in_progress_success(self): + queryset = SingleAuditChecklist.objects.filter(report_id="RPT001") + revert_to_in_progress(self.admin, self.request, queryset) + + self.report1.refresh_from_db() + self.assertEqual(self.report1.submission_status, STATUS.IN_PROGRESS) + messages = [m.message for m in self.request._messages] + self.assertIn( + "Successfully reverted report(s) (RPT001) back to In Progress.", messages + ) + + def test_revert_to_in_progress_failure(self): + queryset = SingleAuditChecklist.objects.filter(report_id="RPT002") + revert_to_in_progress(self.admin, self.request, queryset) + + self.report2.refresh_from_db() + self.assertEqual(self.report2.submission_status, STATUS.IN_PROGRESS) + messages = [m.message for m in self.request._messages] + self.assertIn("Report RPT002 is not flagged for removal.", messages) + + def test_flag_for_removal_success(self): + queryset = SingleAuditChecklist.objects.filter(report_id="RPT002") + flag_for_removal(self.admin, self.request, queryset) + + self.report2.refresh_from_db() + self.assertEqual(self.report2.submission_status, STATUS.FLAGGED_FOR_REMOVAL) + messages = [m.message for m in self.request._messages] + self.assertIn("Successfully flagged report(s) (RPT002) for removal.", messages) + + def test_flag_for_removal_already_flagged(self): + queryset = SingleAuditChecklist.objects.filter(report_id="RPT001") + flag_for_removal(self.admin, self.request, queryset) + + self.report1.refresh_from_db() + self.assertEqual(self.report1.submission_status, STATUS.FLAGGED_FOR_REMOVAL) + messages = [m.message for m in self.request._messages] + self.assertIn("Report(s) (RPT001) were already flagged.", messages) diff --git a/backend/audit/views/views.py b/backend/audit/views/views.py index 1d785128c..c5a87acf7 100644 --- a/backend/audit/views/views.py +++ b/backend/audit/views/views.py @@ -4,7 +4,7 @@ from django.views.decorators.csrf import csrf_exempt from django.shortcuts import render, redirect from django.db import transaction -from django.db.models import F +from django.db.models import F, Q from django.db.transaction import TransactionManagementError from django.core.exceptions import BadRequest, PermissionDenied, ValidationError from django.contrib.auth.mixins import LoginRequiredMixin @@ -125,7 +125,9 @@ def fetch_my_submissions(cls, user): """ accesses = Access.objects.filter(user=user) sac_ids = [access.sac.id for access in accesses] - data = SingleAuditChecklist.objects.filter(id__in=sac_ids).values( + data = SingleAuditChecklist.objects.filter( + Q(id__in=sac_ids) & ~Q(submission_status=STATUS.FLAGGED_FOR_REMOVAL) + ).values( "report_id", "submission_status", auditee_uei=F("general_information__auditee_uei"),