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