From a7f890419f58ee211e4d36a10f3620db7347e492 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?=
 <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de>
Date: Wed, 28 Sep 2022 01:11:10 +0200
Subject: [PATCH] Implement admin actions for constraint violations

Add three admin actions to mark CVs as resolved, or set the level to 'warning' or 'violation' -- each including a preview/confirmation step
Introduce a generic view inheriting from the generic admin intermediate view to handle confirmations for admin actions (performing on multiple, freely selected items)
This implements #154
---
 AKModel/admin.py | 29 ++++++++++++++++++-
 AKModel/forms.py |  4 +++
 AKModel/views.py | 73 ++++++++++++++++++++++++++++++++++++++++++++++--
 3 files changed, 102 insertions(+), 4 deletions(-)

diff --git a/AKModel/admin.py b/AKModel/admin.py
index ae8da12f..4675d842 100644
--- a/AKModel/admin.py
+++ b/AKModel/admin.py
@@ -3,8 +3,9 @@ from django.apps import apps
 from django.contrib import admin
 from django.contrib.admin import SimpleListFilter, RelatedFieldListFilter
 from django.db.models import Count, F
+from django.http import HttpResponseRedirect
 from django.shortcuts import render, redirect
-from django.urls import reverse_lazy
+from django.urls import reverse_lazy, path
 from django.utils import timezone
 from django.utils.html import format_html
 from django.utils.safestring import mark_safe
@@ -17,6 +18,7 @@ from AKModel.availability.models import Availability
 from AKModel.models import Event, AKOwner, AKCategory, AKTrack, AKTag, AKRequirement, AK, AKSlot, Room, AKOrgaMessage, \
     ConstraintViolation
 from AKModel.urls import get_admin_urls_event_wizard, get_admin_urls_event
+from AKModel.views import CVMarkResolvedView, CVSetLevelViolationView, CVSetLevelWarningView
 
 
 class EventRelatedFieldListFilter(RelatedFieldListFilter):
@@ -317,3 +319,28 @@ class ConstraintViolationAdmin(admin.ModelAdmin):
     list_filter = ['event']
     readonly_fields = ['timestamp']
     form = ConstraintViolationAdminForm
+    actions = ['mark_resolved', 'set_violation', 'set_warning']
+
+    def get_urls(self):
+        urls = [
+            path('mark-resolved/', CVMarkResolvedView.as_view(), name="cv-mark-resolved"),
+            path('set-violation/', CVSetLevelViolationView.as_view(), name="cv-set-violation"),
+            path('set-warning/', CVSetLevelWarningView.as_view(), name="cv-set-warning"),
+        ]
+        urls.extend(super().get_urls())
+        return urls
+
+    def mark_resolved(self, request, queryset):
+        selected = queryset.values_list('pk', flat=True)
+        return HttpResponseRedirect(f"{reverse_lazy('admin:cv-mark-resolved')}?pks={','.join(str(pk) for pk in selected)}")
+    mark_resolved.short_description = _("Mark Constraint Violations as manually resolved")
+
+    def set_violation(self, request, queryset):
+        selected = queryset.values_list('pk', flat=True)
+        return HttpResponseRedirect(f"{reverse_lazy('admin:cv-set-violation')}?pks={','.join(str(pk) for pk in selected)}")
+    set_violation.short_description = _('Set to Constraint Violations to level "violation"')
+
+    def set_warning(self, request, queryset):
+        selected = queryset.values_list('pk', flat=True)
+        return HttpResponseRedirect(f"{reverse_lazy('admin:cv-set-warning')}?pks={','.join(str(pk) for pk in selected)}")
+    set_warning.short_description = _('Set Constraint Violations to level "warning"')
diff --git a/AKModel/forms.py b/AKModel/forms.py
index 28e5c678..d72c9737 100644
--- a/AKModel/forms.py
+++ b/AKModel/forms.py
@@ -74,6 +74,10 @@ class AdminIntermediateForm(forms.Form):
     pass
 
 
+class AdminIntermediateActionForm(AdminIntermediateForm):
+    pks = forms.CharField(widget=forms.HiddenInput)
+
+
 class SlideExportForm(AdminIntermediateForm):
     num_next = forms.IntegerField(
         min_value=0,
diff --git a/AKModel/views.py b/AKModel/views.py
index 530a26c5..b6311f3f 100644
--- a/AKModel/views.py
+++ b/AKModel/views.py
@@ -1,10 +1,11 @@
 import os
 import tempfile
+from abc import ABC, abstractmethod
 from itertools import zip_longest
 
 from django.contrib import admin, messages
 from django.shortcuts import get_object_or_404, redirect
-from django.urls import reverse_lazy
+from django.urls import reverse_lazy, reverse
 from django.utils.translation import gettext_lazy as _
 from django.views.generic import TemplateView, DetailView, ListView, DeleteView, CreateView, FormView, UpdateView
 from django_tex.core import render_template_with_context, run_tex_in_directory
@@ -12,8 +13,10 @@ from django_tex.response import PDFResponse
 from rest_framework import viewsets, permissions, mixins
 
 from AKModel.forms import NewEventWizardStartForm, NewEventWizardSettingsForm, NewEventWizardPrepareImportForm, \
-    NewEventWizardImportForm, NewEventWizardActivateForm, AdminIntermediateForm, SlideExportForm
-from AKModel.models import Event, AK, AKSlot, Room, AKTrack, AKCategory, AKOwner, AKOrgaMessage, AKRequirement
+    NewEventWizardImportForm, NewEventWizardActivateForm, AdminIntermediateForm, SlideExportForm, \
+    AdminIntermediateActionForm
+from AKModel.models import Event, AK, AKSlot, Room, AKTrack, AKCategory, AKOwner, AKOrgaMessage, AKRequirement, \
+    ConstraintViolation
 from AKModel.serializers import AKSerializer, AKSlotSerializer, RoomSerializer, AKTrackSerializer, AKCategorySerializer, \
     AKOwnerSerializer
 
@@ -369,3 +372,67 @@ class ExportSlidesView(EventSlugMixin, IntermediateAdminView):
             pdf = run_tex_in_directory(source, tempdir, template_name=self.template_name)
 
         return PDFResponse(pdf, filename='slides.pdf')
+
+
+class IntermediateAdminActionView(IntermediateAdminView, ABC):
+    form_class = AdminIntermediateActionForm
+
+    def get_queryset(self, pks=None):
+        if pks is None:
+            pks = self.request.GET['pks']
+        return self.model.objects.filter(pk__in=pks.split(","))
+
+    def get_initial(self):
+        initial = super().get_initial()
+        initial['pks'] = self.request.GET['pks']
+        return initial
+
+    def get_preview(self):
+        entities = self.get_queryset()
+        joined_entities = '\n'.join(str(e) for e in entities)
+        return f"{self.confirmation_message}:\n\n {joined_entities}"
+
+    def get_success_url(self):
+        return reverse(f"admin:{self.model._meta.app_label}_{self.model._meta.model_name}_changelist")
+
+    @abstractmethod
+    def perform_action(self, entity):
+        pass
+
+    def form_valid(self, form):
+        entities = self.get_queryset(pks=form.cleaned_data['pks'])
+        for entity in entities:
+            self.perform_action(entity)
+            entity.save()
+        messages.add_message(self.request, messages.SUCCESS, self.success_message)
+        return super().form_valid(form)
+
+
+class CVMarkResolvedView(IntermediateAdminActionView):
+    title = _('Mark Constraint Violations as manually resolved')
+    model = ConstraintViolation
+    confirmation_message = _("The following Constraint Violations will be marked as manually resolved")
+    success_message = _("Constraint Violations marked as resolved")
+
+    def perform_action(self, entity):
+        entity.manually_resolved = True
+
+
+class CVSetLevelViolationView(IntermediateAdminActionView):
+    title = _('Set Constraint Violations to level "violation"')
+    model = ConstraintViolation
+    confirmation_message = _("The following Constraint Violations will be set to level 'violation'")
+    success_message = _("Constraint Violations set to level 'violation'")
+
+    def perform_action(self, entity):
+        entity.level = ConstraintViolation.ViolationLevel.VIOLATION
+
+
+class CVSetLevelWarningView(IntermediateAdminActionView):
+    title = _('Set Constraint Violations to level "warning"')
+    model = ConstraintViolation
+    confirmation_message = _("The following Constraint Violations will be set to level 'warning'")
+    success_message = _("Constraint Violations set to level 'warning'")
+
+    def perform_action(self, entity):
+        entity.level = ConstraintViolation.ViolationLevel.WARNING
-- 
GitLab