From ec088aa221ce391ba36938efcf92bd8824d663e0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?=
 <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de>
Date: Tue, 27 Sep 2022 23:53:16 +0200
Subject: [PATCH 1/8] Introduce GUI for slide export

This allows specifying the parameters without the need to know the GET keys
Also resolve double-compiling issue and thus switch from custom version of django-tex to the latest official release
Minor improvements to generic admin action view
This implements #152
---
 AKModel/forms.py                            |  24 +++++
 AKModel/templates/admin/AKModel/status.html |   4 +-
 AKModel/urls.py                             |   5 +-
 AKModel/views.py                            | 103 ++++++++++----------
 requirements.txt                            |   2 +-
 5 files changed, 82 insertions(+), 56 deletions(-)

diff --git a/AKModel/forms.py b/AKModel/forms.py
index 43f712ed..8554efd9 100644
--- a/AKModel/forms.py
+++ b/AKModel/forms.py
@@ -73,3 +73,27 @@ class NewEventWizardActivateForm(forms.ModelForm):
 
 class AdminIntermediateForm(forms.Form):
     pass
+
+
+class SlideExportForm(AdminIntermediateForm):
+    num_next = forms.IntegerField(
+        min_value=0,
+        max_value=6,
+        initial=3,
+        label=_("# next AKs"),
+        help_text=_("How many next AKs should be shown on a slide?"))
+    presentation_mode = forms.TypedChoiceField(
+        initial=False,
+        label=_("Presentation only?"),
+        widget=forms.RadioSelect,
+        choices=((True, _('Yes')), (False, _('No'))),
+        coerce=lambda x: x == "True",
+        help_text=_("Restrict AKs to those that asked for chance to be presented?"))
+    wish_notes = forms.TypedChoiceField(
+        initial=False,
+        label=_("Space for notes in wishes?"),
+        widget=forms.RadioSelect,
+        choices=((True, _('Yes')), (False, _('No'))),
+        coerce=lambda x: x == "True",
+        help_text=_("Create symbols indicating space to note down owners and timeslots for wishes, e.g., to be filled "
+                    "out on a touch screen while presenting?"))
diff --git a/AKModel/templates/admin/AKModel/status.html b/AKModel/templates/admin/AKModel/status.html
index 362c7240..19d753c9 100644
--- a/AKModel/templates/admin/AKModel/status.html
+++ b/AKModel/templates/admin/AKModel/status.html
@@ -89,9 +89,7 @@
                     <a class="btn btn-success"
                        href="{% url 'admin:ak_wiki_export' slug=event.slug %}">{% trans "Export AKs for Wiki" %}</a>
                     <a class="btn btn-success"
-                       href="{% url 'admin:ak_slide_export' event_slug=event.slug %}?num_next=3&wish_notes=False">{% trans "Export AK Slides" %}</a>
-                    <a class="btn btn-success"
-                       href="{% url 'admin:ak_slide_export' event_slug=event.slug %}?num_next=3&presentation_mode">{% trans "Export AK Slides (Presentation AKs only)" %}</a>
+                       href="{% url 'admin:ak_slide_export' event_slug=event.slug %}">{% trans "Export AK Slides" %}</a>
                 {% endif %}
 
                 <h3 class="block-header">{% trans "Requirements" %}</h3>
diff --git a/AKModel/urls.py b/AKModel/urls.py
index ca9cfe67..e86661fd 100644
--- a/AKModel/urls.py
+++ b/AKModel/urls.py
@@ -6,7 +6,7 @@ from rest_framework.routers import DefaultRouter
 from AKModel import views
 from AKModel.views import NewEventWizardStartView, NewEventWizardSettingsView, NewEventWizardPrepareImportView, \
     NewEventWizardImportView, NewEventWizardActivateView, NewEventWizardFinishView, EventStatusView, \
-    AKRequirementOverview, AKCSVExportView, AKWikiExportView, AKMessageDeleteView, export_slides
+    AKRequirementOverview, AKCSVExportView, AKWikiExportView, AKMessageDeleteView, ExportSlidesView
 
 api_router = DefaultRouter()
 api_router.register('akowner', views.AKOwnerViewSet, basename='AKOwner')
@@ -81,6 +81,5 @@ def get_admin_urls_event(admin_site):
              name="ak_wiki_export"),
         path('<slug:event_slug>/delete-orga-messages/', admin_site.admin_view(AKMessageDeleteView.as_view()),
              name="ak_delete_orga_messages"),
-        path('<slug:event_slug>/ak-slide-export/', export_slides, name="ak_slide_export"),
-
+        path('<slug:event_slug>/ak-slide-export/', ExportSlidesView.as_view(), name="ak_slide_export"),
     ]
diff --git a/AKModel/views.py b/AKModel/views.py
index 08f054f1..530a26c5 100644
--- a/AKModel/views.py
+++ b/AKModel/views.py
@@ -1,18 +1,18 @@
-from abc import ABC, abstractmethod
+import os
+import tempfile
 from itertools import zip_longest
 
 from django.contrib import admin, messages
-from django.contrib.admin.views.decorators import staff_member_required
-from django.http import HttpResponseRedirect
 from django.shortcuts import get_object_or_404, redirect
 from django.urls import reverse_lazy
 from django.utils.translation import gettext_lazy as _
 from django.views.generic import TemplateView, DetailView, ListView, DeleteView, CreateView, FormView, UpdateView
-from django_tex.shortcuts import render_to_pdf
+from django_tex.core import render_template_with_context, run_tex_in_directory
+from django_tex.response import PDFResponse
 from rest_framework import viewsets, permissions, mixins
 
 from AKModel.forms import NewEventWizardStartForm, NewEventWizardSettingsForm, NewEventWizardPrepareImportForm, \
-    NewEventWizardImportForm, NewEventWizardActivateForm, AdminIntermediateForm
+    NewEventWizardImportForm, NewEventWizardActivateForm, AdminIntermediateForm, SlideExportForm
 from AKModel.models import Event, AK, AKSlot, Room, AKTrack, AKCategory, AKOwner, AKOrgaMessage, AKRequirement
 from AKModel.serializers import AKSerializer, AKSlotSerializer, RoomSerializer, AKTrackSerializer, AKCategorySerializer, \
     AKOwnerSerializer
@@ -195,13 +195,12 @@ class AKWikiExportView(AdminViewMixin, DetailView):
         return context
 
 
-class IntermediateAdminView(AdminViewMixin, FormView, ABC):
+class IntermediateAdminView(AdminViewMixin, FormView):
     template_name = "admin/AKModel/action_intermediate.html"
     form_class = AdminIntermediateForm
 
-    @abstractmethod
     def get_preview(self):
-        pass
+        return ""
 
     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
@@ -217,9 +216,6 @@ class AKMessageDeleteView(EventSlugMixin, IntermediateAdminView):
     def get_orga_messages_for_event(self, event):
         return AKOrgaMessage.objects.filter(ak__event=event)
 
-    def get_preview(self):
-        return None
-
     def get_success_url(self):
         return reverse_lazy('admin:event_status', kwargs={'slug': self.event.slug})
 
@@ -326,41 +322,50 @@ class NewEventWizardFinishView(WizardViewMixin, DetailView):
     wizard_step = 6
 
 
-@staff_member_required
-def export_slides(request, event_slug):
-    template_name = 'admin/AKModel/export/slides.tex'
-
-    event = get_object_or_404(Event, slug=event_slug)
-
-    NEXT_AK_LIST_LENGTH = int(request.GET["num_next"]) if "num_next" in request.GET else 3
-    RESULT_PRESENTATION_MODE = True if "presentation_mode" in request.GET else False
-    SPACE_FOR_NOTES_IN_WISHES = request.GET["wish_notes"] == "True" if "wish_notes" in request.GET else False
-
-    translations = {
-        'symbols': _("Symbols"),
-        'who': _("Who?"),
-        'duration': _("Duration(s)"),
-        'reso': _("Reso intention?"),
-        'category': _("Category (for Wishes)"),
-        'wishes': _("Wishes"),
-    }
-
-    def build_ak_list_with_next_aks(ak_list):
-        next_aks_list = zip_longest(*[ak_list[i + 1:] for i in range(NEXT_AK_LIST_LENGTH)], fillvalue=None)
-        return [(ak, next_aks) for ak, next_aks in zip_longest(ak_list, next_aks_list, fillvalue=list())]
-
-    categories_with_aks, ak_wishes = event.get_categories_with_aks(wishes_seperately=True, filter=lambda
-        ak: not RESULT_PRESENTATION_MODE or (ak.present or (ak.present is None and ak.category.present_by_default)))
-
-    context = {
-        'title': event.name,
-        'categories_with_aks': [(category, build_ak_list_with_next_aks(ak_list)) for category, ak_list in
-                                categories_with_aks],
-        'subtitle': _("AKs"),
-        "wishes": build_ak_list_with_next_aks(ak_wishes),
-        "translations": translations,
-        "result_presentation_mode": RESULT_PRESENTATION_MODE,
-        "space_for_notes_in_wishes": SPACE_FOR_NOTES_IN_WISHES,
-    }
-
-    return render_to_pdf(request, template_name, context, filename='slides.pdf')
+class ExportSlidesView(EventSlugMixin, IntermediateAdminView):
+    title = _('Export AK Slides')
+    form_class = SlideExportForm
+
+    def form_valid(self, form):
+        template_name = 'admin/AKModel/export/slides.tex'
+
+        NEXT_AK_LIST_LENGTH = form.cleaned_data['num_next']
+        RESULT_PRESENTATION_MODE = form.cleaned_data["presentation_mode"]
+        SPACE_FOR_NOTES_IN_WISHES = form.cleaned_data["wish_notes"]
+
+        translations = {
+            'symbols': _("Symbols"),
+            'who': _("Who?"),
+            'duration': _("Duration(s)"),
+            'reso': _("Reso intention?"),
+            'category': _("Category (for Wishes)"),
+            'wishes': _("Wishes"),
+        }
+
+        def build_ak_list_with_next_aks(ak_list):
+            next_aks_list = zip_longest(*[ak_list[i + 1:] for i in range(NEXT_AK_LIST_LENGTH)], fillvalue=None)
+            return [(ak, next_aks) for ak, next_aks in zip_longest(ak_list, next_aks_list, fillvalue=list())]
+
+        categories_with_aks, ak_wishes = self.event.get_categories_with_aks(wishes_seperately=True, filter=lambda
+            ak: not RESULT_PRESENTATION_MODE or (ak.present or (ak.present is None and ak.category.present_by_default)))
+
+        context = {
+            'title': self.event.name,
+            'categories_with_aks': [(category, build_ak_list_with_next_aks(ak_list)) for category, ak_list in
+                                    categories_with_aks],
+            'subtitle': _("AKs"),
+            "wishes": build_ak_list_with_next_aks(ak_wishes),
+            "translations": translations,
+            "result_presentation_mode": RESULT_PRESENTATION_MODE,
+            "space_for_notes_in_wishes": SPACE_FOR_NOTES_IN_WISHES,
+        }
+
+        source = render_template_with_context(template_name, context)
+
+        # Perform real compilation (run latex twice for correct page numbers)
+        with tempfile.TemporaryDirectory() as tempdir:
+            run_tex_in_directory(source, tempdir, template_name=self.template_name)
+            os.remove(f'{tempdir}/texput.tex')
+            pdf = run_tex_in_directory(source, tempdir, template_name=self.template_name)
+
+        return PDFResponse(pdf, filename='slides.pdf')
diff --git a/requirements.txt b/requirements.txt
index 2271a467..5b8693e8 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -8,7 +8,7 @@ django-simple-history==3.1.1
 django-registration-redux==2.11
 django-debug-toolbar==3.7.0
 django-bootstrap-datepicker-plus==3.0.5
-django-tex @ git+https://github.com/bhaettasch/django-tex.git@66cc6567acde4db2ac971b7707652067e664392c
+django-tex==1.1.10
 django-csp==3.7
 mysqlclient==2.0.3  # for production deployment
 pytz==2022.4
-- 
GitLab


From f28e9606d4d40c427bdeee38f4a121e4335c44d6 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 2/8] 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 39fc678d..61a590ab 100644
--- a/AKModel/admin.py
+++ b/AKModel/admin.py
@@ -4,8 +4,9 @@ from django.contrib import admin, messages
 from django.contrib.admin import SimpleListFilter, RelatedFieldListFilter, action
 from django.db.models import Count, F
 from django.db.models.functions import Now
+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
@@ -18,6 +19,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):
@@ -329,3 +331,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 8554efd9..3aa94868 100644
--- a/AKModel/forms.py
+++ b/AKModel/forms.py
@@ -75,6 +75,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


From 6e8766607180c0f1cb792a1d5fc7e75d441fa230 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?=
 <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de>
Date: Sun, 2 Oct 2022 14:57:06 +0200
Subject: [PATCH 3/8] Implement admin actions for AKs

Add two admin actions to reset interest and interest counter of AKs -- each including a preview/confirmation step
This implements #153
---
 AKModel/admin.py | 23 +++++++++++++++++++++--
 AKModel/views.py | 20 ++++++++++++++++++++
 2 files changed, 41 insertions(+), 2 deletions(-)

diff --git a/AKModel/admin.py b/AKModel/admin.py
index 61a590ab..2f49587c 100644
--- a/AKModel/admin.py
+++ b/AKModel/admin.py
@@ -19,7 +19,8 @@ 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
+from AKModel.views import CVMarkResolvedView, CVSetLevelViolationView, CVSetLevelWarningView, AKResetInterestView, \
+    AKResetInterestCounterView
 
 
 class EventRelatedFieldListFilter(RelatedFieldListFilter):
@@ -187,7 +188,7 @@ class AKAdmin(SimpleHistoryAdmin):
     list_filter = ['event', WishFilter, ('category', EventRelatedFieldListFilter), ('requirements', EventRelatedFieldListFilter)]
     list_editable = ['short_name', 'track', 'interest_counter']
     ordering = ['pk']
-    actions = ['wiki_export']
+    actions = ['wiki_export', 'reset_interest', 'reset_interest_counter']
     form = AKAdminForm
 
     def is_wish(self, obj):
@@ -205,6 +206,24 @@ class AKAdmin(SimpleHistoryAdmin):
             kwargs['initial'] = Event.get_next_active()
         return super(AKAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
 
+    def get_urls(self):
+        urls = [
+            path('reset-interest/', AKResetInterestView.as_view(), name="ak-reset-interest"),
+            path('reset-interest-counter/', AKResetInterestCounterView.as_view(), name="ak-reset-interest-counter"),
+        ]
+        urls.extend(super().get_urls())
+        return urls
+
+    def reset_interest(self, request, queryset):
+        selected = queryset.values_list('pk', flat=True)
+        return HttpResponseRedirect(f"{reverse_lazy('admin:ak-reset-interest')}?pks={','.join(str(pk) for pk in selected)}")
+    reset_interest.short_description = _("Reset interest in AKs")
+
+    def reset_interest_counter(self, request, queryset):
+        selected = queryset.values_list('pk', flat=True)
+        return HttpResponseRedirect(f"{reverse_lazy('admin:ak-reset-interest-counter')}?pks={','.join(str(pk) for pk in selected)}")
+    reset_interest_counter.short_description = _("Reset AKs' interest counters")
+
 
 class RoomForm(AvailabilitiesFormMixin, forms.ModelForm):
     class Meta:
diff --git a/AKModel/views.py b/AKModel/views.py
index b6311f3f..a766d896 100644
--- a/AKModel/views.py
+++ b/AKModel/views.py
@@ -436,3 +436,23 @@ class CVSetLevelWarningView(IntermediateAdminActionView):
 
     def perform_action(self, entity):
         entity.level = ConstraintViolation.ViolationLevel.WARNING
+
+
+class AKResetInterestView(IntermediateAdminActionView):
+    title = _("Reset interest in AKs")
+    model = AK
+    confirmation_message = _("Interest of the following AKs will be set to not filled (-1):")
+    success_message = _("Reset of interest in AKs successful.")
+
+    def perform_action(self, entity):
+        entity.interest = -1
+
+
+class AKResetInterestCounterView(IntermediateAdminActionView):
+    title = _("Reset AKs' interest counters")
+    model = AK
+    confirmation_message = _("Interest counter of the following AKs will be set to 0:")
+    success_message = _("AKs' interest counters set back to 0.")
+
+    def perform_action(self, entity):
+        entity.interest_counter = 0
-- 
GitLab


From d77161f9dfdb0fae5deb5caa3049a3304f37c123 Mon Sep 17 00:00:00 2001
From: Nadja Geisler <ngeisler@fachschaft.informatik.tu-darmstadt.de>
Date: Sun, 23 Oct 2022 21:24:13 +0200
Subject: [PATCH 4/8] update AKModel translations

---
 AKModel/admin.py                           |   2 +-
 AKModel/locale/de_DE/LC_MESSAGES/django.po | 177 ++++++++++++++++-----
 2 files changed, 135 insertions(+), 44 deletions(-)

diff --git a/AKModel/admin.py b/AKModel/admin.py
index 2f49587c..2168b93a 100644
--- a/AKModel/admin.py
+++ b/AKModel/admin.py
@@ -369,7 +369,7 @@ class ConstraintViolationAdmin(admin.ModelAdmin):
     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"')
+    set_violation.short_description = _('Set Constraint Violations to level "violation"')
 
     def set_warning(self, request, queryset):
         selected = queryset.values_list('pk', flat=True)
diff --git a/AKModel/locale/de_DE/LC_MESSAGES/django.po b/AKModel/locale/de_DE/LC_MESSAGES/django.po
index fd79e07e..63f87879 100644
--- a/AKModel/locale/de_DE/LC_MESSAGES/django.po
+++ b/AKModel/locale/de_DE/LC_MESSAGES/django.po
@@ -2,7 +2,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2022-10-23 18:03+0000\n"
+"POT-Creation-Date: 2022-10-23 19:20+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -11,7 +11,7 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: AKModel/admin.py:58 AKModel/admin.py:60
+#: AKModel/admin.py:61 AKModel/admin.py:63
 #: AKModel/templates/admin/AKModel/event_wizard/activate.html:32
 #: AKModel/templates/admin/AKModel/event_wizard/created_prepare_import.html:48
 #: AKModel/templates/admin/AKModel/event_wizard/finish.html:21
@@ -21,42 +21,62 @@ msgstr ""
 msgid "Status"
 msgstr "Status"
 
-#: AKModel/admin.py:67
+#: AKModel/admin.py:70
 msgid "Publish plan"
 msgstr "Plan veröffentlichen"
 
-#: AKModel/admin.py:70
+#: AKModel/admin.py:73
 msgid "Plan published"
 msgstr "Plan veröffentlicht"
 
-#: AKModel/admin.py:72
+#: AKModel/admin.py:75
 msgid "Unpublish plan"
 msgstr "Plan verbergen"
 
-#: AKModel/admin.py:75
+#: AKModel/admin.py:78
 msgid "Plan unpublished"
 msgstr "Plan verborgen"
 
-#: AKModel/admin.py:144
+#: AKModel/admin.py:147
 msgid "Wish"
 msgstr "AK-Wunsch"
 
-#: AKModel/admin.py:150
+#: AKModel/admin.py:153
 msgid "Is wish"
 msgstr "Ist ein Wunsch"
 
-#: AKModel/admin.py:151
+#: AKModel/admin.py:154
 msgid "Is not a wish"
 msgstr "Ist kein Wunsch"
 
-#: AKModel/admin.py:197
+#: AKModel/admin.py:200
 msgid "Export to wiki syntax"
 msgstr "In Wiki-Syntax exportieren"
 
-#: AKModel/admin.py:291
+#: AKModel/admin.py:220 AKModel/views.py:442
+msgid "Reset interest in AKs"
+msgstr "Interesse an AKs zurücksetzen"
+
+#: AKModel/admin.py:225 AKModel/views.py:452
+msgid "Reset AKs' interest counters"
+msgstr "Interessenszähler der AKs zurücksetzen"
+
+#: AKModel/admin.py:312
 msgid "AK Details"
 msgstr "AK-Details"
 
+#: AKModel/admin.py:367 AKModel/views.py:412
+msgid "Mark Constraint Violations as manually resolved"
+msgstr "Markiere Constraintverletzungen als manuell behoben"
+
+#: AKModel/admin.py:372 AKModel/views.py:422
+msgid "Set Constraint Violations to level \"violation\""
+msgstr "Constraintverletzungen auf Level \"Violation\" setzen"
+
+#: AKModel/admin.py:377 AKModel/views.py:432
+msgid "Set Constraint Violations to level \"warning\""
+msgstr "Constraintverletzungen auf Level \"Warning\" setzen"
+
 #: AKModel/availability/forms.py:21 AKModel/availability/models.py:248
 msgid "Availability"
 msgstr "Verfügbarkeit"
@@ -151,6 +171,43 @@ msgstr "AK-Kategorien kopieren"
 msgid "Copy ak requirements"
 msgstr "AK-Anforderungen kopieren"
 
+#: AKModel/forms.py:87
+msgid "# next AKs"
+msgstr "# nächste AKs"
+
+#: AKModel/forms.py:88
+msgid "How many next AKs should be shown on a slide?"
+msgstr "Wie viele nächste AKs sollen auf einer Folie angezeigt werden?"
+
+#: AKModel/forms.py:91
+msgid "Presentation only?"
+msgstr "Nur Vorstellung?"
+
+#: AKModel/forms.py:93 AKModel/forms.py:100
+msgid "Yes"
+msgstr "Ja"
+
+#: AKModel/forms.py:93 AKModel/forms.py:100
+msgid "No"
+msgstr "Nein"
+
+#: AKModel/forms.py:95
+msgid "Restrict AKs to those that asked for chance to be presented?"
+msgstr "AKs auf solche, die um eine Vorstellung gebeten haben, einschränken?"
+
+#: AKModel/forms.py:98
+msgid "Space for notes in wishes?"
+msgstr "Platz für Notizen bei den Wünschen?"
+
+#: AKModel/forms.py:102
+msgid ""
+"Create symbols indicating space to note down owners and timeslots for "
+"wishes, e.g., to be filled out on a touch screen while presenting?"
+msgstr ""
+"Symbole anlegen, die Raum zum Notieren von Leitungen und Zeitslots "
+"fürWünsche markieren, z.B. um während der Präsentation auf einem Touchscreen "
+"ausgefüllt zu werden?"
+
 #: AKModel/models.py:18 AKModel/models.py:173 AKModel/models.py:197
 #: AKModel/models.py:216 AKModel/models.py:230 AKModel/models.py:248
 #: AKModel/models.py:344
@@ -187,7 +244,7 @@ msgstr "Zeitzone"
 msgid "Time Zone where this event takes place in"
 msgstr "Zeitzone in der das Event stattfindet"
 
-#: AKModel/models.py:27 AKModel/views.py:242
+#: AKModel/models.py:27 AKModel/views.py:241
 msgid "Start"
 msgstr "Start"
 
@@ -483,7 +540,7 @@ msgstr "AK präsentieren"
 msgid "Present results of this AK"
 msgstr "Die Ergebnisse dieses AKs vorstellen"
 
-#: AKModel/models.py:271 AKModel/templates/admin/AKModel/status.html:97
+#: AKModel/models.py:271 AKModel/templates/admin/AKModel/status.html:95
 msgid "Requirements"
 msgstr "Anforderungen"
 
@@ -513,10 +570,6 @@ msgid "Organizational Notes"
 msgstr "Notizen zur Organisation"
 
 #: AKModel/models.py:280
-#, fuzzy
-#| msgid ""
-#| "Notes to organizers. These are public. For private notes, please send an "
-#| "e-mail."
 msgid ""
 "Notes to organizers. These are public. For private notes, please use the "
 "button for private messages on the detail page of this AK (after creation/"
@@ -829,7 +882,7 @@ msgid "Successfully imported.<br><br>Do you want to activate your event now?"
 msgstr "Erfolgreich importiert.<br><br>Soll das Event jetzt aktiviert werden?"
 
 #: AKModel/templates/admin/AKModel/event_wizard/activate.html:27
-#: AKModel/views.py:247
+#: AKModel/views.py:246
 msgid "Finish"
 msgstr "Abschluss"
 
@@ -894,7 +947,7 @@ msgid "No AKs with this requirement"
 msgstr "Kein AK mit dieser Anforderung"
 
 #: AKModel/templates/admin/AKModel/requirements_overview.html:45
-#: AKModel/templates/admin/AKModel/status.html:113
+#: AKModel/templates/admin/AKModel/status.html:111
 msgid "Add Requirement"
 msgstr "Anforderung hinzufügen"
 
@@ -955,27 +1008,23 @@ msgstr "AKs als CSV exportieren"
 msgid "Export AKs for Wiki"
 msgstr "AKs im Wiki-Format exportieren"
 
-#: AKModel/templates/admin/AKModel/status.html:92
+#: AKModel/templates/admin/AKModel/status.html:92 AKModel/views.py:329
 msgid "Export AK Slides"
 msgstr "AK-Folien exportieren"
 
-#: AKModel/templates/admin/AKModel/status.html:94
-msgid "Export AK Slides (Presentation AKs only)"
-msgstr "AK-Folien exportieren (Nur zu präsentierende AKs)"
-
-#: AKModel/templates/admin/AKModel/status.html:99
+#: AKModel/templates/admin/AKModel/status.html:97
 msgid "No requirements yet"
 msgstr "Bisher keine Anforderungen"
 
-#: AKModel/templates/admin/AKModel/status.html:112
+#: AKModel/templates/admin/AKModel/status.html:110
 msgid "Show AKs for requirements"
 msgstr "Zu Anforderungen gehörige AKs anzeigen"
 
-#: AKModel/templates/admin/AKModel/status.html:116
+#: AKModel/templates/admin/AKModel/status.html:114
 msgid "Messages"
 msgstr "Nachrichten"
 
-#: AKModel/templates/admin/AKModel/status.html:118
+#: AKModel/templates/admin/AKModel/status.html:116
 msgid "Delete all messages"
 msgstr "Alle Nachrichten löschen"
 
@@ -1012,58 +1061,56 @@ msgstr "Login"
 msgid "Register"
 msgstr "Registrieren"
 
-#: AKModel/views.py:144
+#: AKModel/views.py:147
 msgid "Event Status"
 msgstr "Eventstatus"
 
-#: AKModel/views.py:157
+#: AKModel/views.py:160
 msgid "Requirements for Event"
 msgstr "Anforderungen für das Event"
 
-#: AKModel/views.py:171
+#: AKModel/views.py:174
 msgid "AK CSV Export"
 msgstr "AK-CSV-Export"
 
-#: AKModel/views.py:185
+#: AKModel/views.py:188
 msgid "AK Wiki Export"
 msgstr "AK-Wiki-Export"
 
-#: AKModel/views.py:193 AKModel/views.py:345
+#: AKModel/views.py:196 AKModel/views.py:345
 msgid "Wishes"
 msgstr "Wünsche"
 
-#: AKModel/views.py:215
+#: AKModel/views.py:217
 msgid "Delete AK Orga Messages"
 msgstr "AK-Organachrichten löschen"
 
-#: AKModel/views.py:233
+#: AKModel/views.py:232
 msgid "AK Orga Messages successfully deleted"
 msgstr "AK-Organachrichten erfolgreich gelöscht"
 
-#: AKModel/views.py:243
+#: AKModel/views.py:242
 msgid "Settings"
 msgstr "Einstellungen"
 
-#: AKModel/views.py:244
+#: AKModel/views.py:243
 msgid "Event created, Prepare Import"
 msgstr "Event angelegt, Import vorbereiten"
 
-#: AKModel/views.py:245
+#: AKModel/views.py:244
 msgid "Import categories & requirements"
 msgstr "Kategorien & Anforderungen kopieren"
 
-#: AKModel/views.py:246
-#, fuzzy
-#| msgid "Active State"
+#: AKModel/views.py:245
 msgid "Activate?"
 msgstr "Aktivieren?"
 
-#: AKModel/views.py:305
+#: AKModel/views.py:304
 #, python-format
 msgid "Copied '%(obj)s'"
 msgstr "'%(obj)s' kopiert"
 
-#: AKModel/views.py:308
+#: AKModel/views.py:307
 #, python-format
 msgid "Could not copy '%(obj)s' (%(error)s)"
 msgstr "'%(obj)s' konnte nicht kopiert werden (%(error)s)"
@@ -1087,3 +1134,47 @@ msgstr "Resolutionsabsicht?"
 #: AKModel/views.py:344
 msgid "Category (for Wishes)"
 msgstr "Kategorie (für Wünsche)"
+
+#: AKModel/views.py:414
+msgid "The following Constraint Violations will be marked as manually resolved"
+msgstr ""
+"Die folgenden Constraintverletzungen werden als manuell behoben markiert."
+
+#: AKModel/views.py:415
+msgid "Constraint Violations marked as resolved"
+msgstr "Constraintverletzungen als manuell behoben markiert"
+
+#: AKModel/views.py:424
+msgid "The following Constraint Violations will be set to level 'violation'"
+msgstr ""
+"Die folgenden Constraintverletzungen werden auf das Level \"Violation\" "
+"gesetzt."
+
+#: AKModel/views.py:425
+msgid "Constraint Violations set to level 'violation'"
+msgstr "Constraintverletzungen auf Level \"Violation\" gesetzt"
+
+#: AKModel/views.py:434
+msgid "The following Constraint Violations will be set to level 'warning'"
+msgstr ""
+"Die folgenden Constraintverletzungen werden auf das Level 'warning' gesetzt."
+
+#: AKModel/views.py:435
+msgid "Constraint Violations set to level 'warning'"
+msgstr "Constraintverletzungen auf Level \"Warning\" gesetzt"
+
+#: AKModel/views.py:444
+msgid "Interest of the following AKs will be set to not filled (-1):"
+msgstr "Interesse an den folgenden AKs wird auf nicht ausgefüllt (-1) gesetzt:"
+
+#: AKModel/views.py:445
+msgid "Reset of interest in AKs successful."
+msgstr "Interesse an AKs erfolgreich zurückgesetzt."
+
+#: AKModel/views.py:454
+msgid "Interest counter of the following AKs will be set to 0:"
+msgstr "Interessensbekundungszähler der folgenden AKs wird auf 0 gesetzt:"
+
+#: AKModel/views.py:455
+msgid "AKs' interest counters set back to 0."
+msgstr "Interessenszähler der AKs zurückgesetzt"
-- 
GitLab


From 165088c0b193e17630662b224e5dfcc91135ffd0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?=
 <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de>
Date: Sun, 23 Oct 2022 23:06:36 +0200
Subject: [PATCH 5/8] Use action and display decorators

Leverage the decorators introduced in django 3.2 for methods that do not use them already
---
 AKModel/admin.py | 24 ++++++++++--------------
 1 file changed, 10 insertions(+), 14 deletions(-)

diff --git a/AKModel/admin.py b/AKModel/admin.py
index 2168b93a..3bfa7af5 100644
--- a/AKModel/admin.py
+++ b/AKModel/admin.py
@@ -1,7 +1,7 @@
 from django import forms
 from django.apps import apps
 from django.contrib import admin, messages
-from django.contrib.admin import SimpleListFilter, RelatedFieldListFilter, action
+from django.contrib.admin import SimpleListFilter, RelatedFieldListFilter, action, display
 from django.db.models import Count, F
 from django.db.models.functions import Now
 from django.http import HttpResponseRedirect
@@ -56,12 +56,11 @@ class EventAdmin(admin.ModelAdmin):
         urls.extend(super().get_urls())
         return urls
 
+    @display(description=_("Status"))
     def status_url(self, obj):
         return format_html("<a href='{url}'>{text}</a>",
                            url=reverse_lazy('admin:event_status', kwargs={'slug': obj.slug}), text=_("Status"))
 
-    status_url.short_description = _("Status")
-
     def get_form(self, request, obj=None, change=False, **kwargs):
         # Use timezone of event
         timezone.activate(obj.timezone)
@@ -191,16 +190,14 @@ class AKAdmin(SimpleHistoryAdmin):
     actions = ['wiki_export', 'reset_interest', 'reset_interest_counter']
     form = AKAdminForm
 
+    @display(boolean=True)
     def is_wish(self, obj):
         return obj.wish
 
+    @action(description=_("Export to wiki syntax"))
     def wiki_export(self, request, queryset):
         return render(request, 'admin/AKModel/wiki_export.html', context={"AKs": queryset})
 
-    wiki_export.short_description = _("Export to wiki syntax")
-
-    is_wish.boolean = True
-
     def formfield_for_foreignkey(self, db_field, request, **kwargs):
         if db_field.name == 'event':
             kwargs['initial'] = Event.get_next_active()
@@ -214,15 +211,15 @@ class AKAdmin(SimpleHistoryAdmin):
         urls.extend(super().get_urls())
         return urls
 
+    @action(description=_("Reset interest in AKs"))
     def reset_interest(self, request, queryset):
         selected = queryset.values_list('pk', flat=True)
         return HttpResponseRedirect(f"{reverse_lazy('admin:ak-reset-interest')}?pks={','.join(str(pk) for pk in selected)}")
-    reset_interest.short_description = _("Reset interest in AKs")
 
+    @action(description=_("Reset AKs' interest counters"))
     def reset_interest_counter(self, request, queryset):
         selected = queryset.values_list('pk', flat=True)
         return HttpResponseRedirect(f"{reverse_lazy('admin:ak-reset-interest-counter')}?pks={','.join(str(pk) for pk in selected)}")
-    reset_interest_counter.short_description = _("Reset AKs' interest counters")
 
 
 class RoomForm(AvailabilitiesFormMixin, forms.ModelForm):
@@ -303,14 +300,13 @@ class AKSlotAdmin(admin.ModelAdmin):
             kwargs['initial'] = Event.get_next_active()
         return super(AKSlotAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
 
+    @display(description=_('AK Details'))
     def ak_details_link(self, akslot):
         if apps.is_installed("AKSubmission") and akslot.ak is not None:
             link = f"<a href={reverse('submit:ak_detail', args=[akslot.event.slug, akslot.ak.pk])}>{str(akslot.ak)}</a>"
             return mark_safe(link)
         return "-"
 
-    ak_details_link.short_description = _('AK Details')
-
 
 @admin.register(Availability)
 class AvailabilityAdmin(admin.ModelAdmin):
@@ -361,17 +357,17 @@ class ConstraintViolationAdmin(admin.ModelAdmin):
         urls.extend(super().get_urls())
         return urls
 
+    @action(description=_("Mark Constraint Violations as manually resolved"))
     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")
 
+    @action(description=_('Set Constraint Violations to level "violation"'))
     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 Constraint Violations to level "violation"')
 
+    @action(description=_('Set Constraint Violations to level "warning"'))
     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"')
-- 
GitLab


From 19e46342493e68d70f468bad625272f15dd8460f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?=
 <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de>
Date: Sun, 23 Oct 2022 23:26:47 +0200
Subject: [PATCH 6/8] Fix admin action for wiki export

Looks like this was broken since the switch to individual boxes per category.
Fixed it and made it more robust (show error message when trying to export AKs from more than one event at the same time).
---
 AKModel/admin.py                           |  9 +++++-
 AKModel/locale/de_DE/LC_MESSAGES/django.po | 32 ++++++++++++----------
 AKModel/models.py                          |  8 ++++--
 3 files changed, 31 insertions(+), 18 deletions(-)

diff --git a/AKModel/admin.py b/AKModel/admin.py
index 3bfa7af5..1bc4a653 100644
--- a/AKModel/admin.py
+++ b/AKModel/admin.py
@@ -196,7 +196,14 @@ class AKAdmin(SimpleHistoryAdmin):
 
     @action(description=_("Export to wiki syntax"))
     def wiki_export(self, request, queryset):
-        return render(request, 'admin/AKModel/wiki_export.html', context={"AKs": queryset})
+        # Only export when all AKs belong to the same event
+        if queryset.values("event").distinct().count() == 1:
+            event = queryset.first().event
+            pks = set(ak.pk for ak in queryset.all())
+            categories_with_aks = event.get_categories_with_aks(wishes_seperately=False, filter=lambda ak: ak.pk in pks,
+                                                                hide_empty_categories=True)
+            return render(request, 'admin/AKModel/wiki_export.html', context={"categories_with_aks": categories_with_aks})
+        self.message_user(request, _("Cannot export AKs from more than one event at the same time."), messages.ERROR)
 
     def formfield_for_foreignkey(self, db_field, request, **kwargs):
         if db_field.name == 'event':
diff --git a/AKModel/locale/de_DE/LC_MESSAGES/django.po b/AKModel/locale/de_DE/LC_MESSAGES/django.po
index 63f87879..ea136ff1 100644
--- a/AKModel/locale/de_DE/LC_MESSAGES/django.po
+++ b/AKModel/locale/de_DE/LC_MESSAGES/django.po
@@ -2,7 +2,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2022-10-23 19:20+0000\n"
+"POT-Creation-Date: 2022-10-23 23:19+0200\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -11,7 +11,7 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: AKModel/admin.py:61 AKModel/admin.py:63
+#: AKModel/admin.py:59 AKModel/admin.py:62
 #: AKModel/templates/admin/AKModel/event_wizard/activate.html:32
 #: AKModel/templates/admin/AKModel/event_wizard/created_prepare_import.html:48
 #: AKModel/templates/admin/AKModel/event_wizard/finish.html:21
@@ -21,38 +21,42 @@ msgstr ""
 msgid "Status"
 msgstr "Status"
 
-#: AKModel/admin.py:70
+#: AKModel/admin.py:69
 msgid "Publish plan"
 msgstr "Plan veröffentlichen"
 
-#: AKModel/admin.py:73
+#: AKModel/admin.py:72
 msgid "Plan published"
 msgstr "Plan veröffentlicht"
 
-#: AKModel/admin.py:75
+#: AKModel/admin.py:74
 msgid "Unpublish plan"
 msgstr "Plan verbergen"
 
-#: AKModel/admin.py:78
+#: AKModel/admin.py:77
 msgid "Plan unpublished"
 msgstr "Plan verborgen"
 
-#: AKModel/admin.py:147
+#: AKModel/admin.py:146
 msgid "Wish"
 msgstr "AK-Wunsch"
 
-#: AKModel/admin.py:153
+#: AKModel/admin.py:152
 msgid "Is wish"
 msgstr "Ist ein Wunsch"
 
-#: AKModel/admin.py:154
+#: AKModel/admin.py:153
 msgid "Is not a wish"
 msgstr "Ist kein Wunsch"
 
-#: AKModel/admin.py:200
+#: AKModel/admin.py:197
 msgid "Export to wiki syntax"
 msgstr "In Wiki-Syntax exportieren"
 
+#: AKModel/admin.py:205
+msgid "Cannot export AKs from more than one event at the same time."
+msgstr "Kann nicht AKs von mehreren Events zur selben Zeit exportieren."
+
 #: AKModel/admin.py:220 AKModel/views.py:442
 msgid "Reset interest in AKs"
 msgstr "Interesse an AKs zurücksetzen"
@@ -61,19 +65,19 @@ msgstr "Interesse an AKs zurücksetzen"
 msgid "Reset AKs' interest counters"
 msgstr "Interessenszähler der AKs zurücksetzen"
 
-#: AKModel/admin.py:312
+#: AKModel/admin.py:309
 msgid "AK Details"
 msgstr "AK-Details"
 
-#: AKModel/admin.py:367 AKModel/views.py:412
+#: AKModel/admin.py:366 AKModel/views.py:412
 msgid "Mark Constraint Violations as manually resolved"
 msgstr "Markiere Constraintverletzungen als manuell behoben"
 
-#: AKModel/admin.py:372 AKModel/views.py:422
+#: AKModel/admin.py:371 AKModel/views.py:422
 msgid "Set Constraint Violations to level \"violation\""
 msgstr "Constraintverletzungen auf Level \"Violation\" setzen"
 
-#: AKModel/admin.py:377 AKModel/views.py:432
+#: AKModel/admin.py:376 AKModel/views.py:432
 msgid "Set Constraint Violations to level \"warning\""
 msgstr "Constraintverletzungen auf Level \"Warning\" setzen"
 
diff --git a/AKModel/models.py b/AKModel/models.py
index dee3b93e..a1896eac 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -73,7 +73,7 @@ class Event(models.Model):
             event = Event.objects.order_by('start').filter(start__gt=datetime.now()).first()
         return event
 
-    def get_categories_with_aks(self, wishes_seperately=False, filter=lambda ak: True):
+    def get_categories_with_aks(self, wishes_seperately=False, filter=lambda ak: True, hide_empty_categories=False):
         """
         Get AKCategories as well as a list of AKs belonging to the category for this event
 
@@ -97,7 +97,8 @@ class Event(models.Model):
                     else:
                         if filter(ak):
                             ak_list.append(ak)
-                categories_with_aks.append((category, ak_list))
+                if not hide_empty_categories or len(ak_list) > 0:
+                    categories_with_aks.append((category, ak_list))
             return categories_with_aks, ak_wishes
         else:
             for category in categories:
@@ -105,7 +106,8 @@ class Event(models.Model):
                 for ak in category.ak_set.all():
                     if filter(ak):
                         ak_list.append(ak)
-                categories_with_aks.append((category, ak_list))
+                if not hide_empty_categories or len(ak_list) > 0:
+                    categories_with_aks.append((category, ak_list))
             return categories_with_aks
 
     def get_unscheduled_wish_slots(self):
-- 
GitLab


From 0bc73445a719f6c8c3948d0b452dccb0d1442900 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?=
 <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de>
Date: Sun, 23 Oct 2022 23:44:46 +0200
Subject: [PATCH 7/8] Increase performance

Move loop-based action code to mixin that will only be used when more complex actions should be performed
Use queryset update function on existing actions to perform the necessary updating in only one db call instead of one query per entity
Additionally, visualize manually_resolved status in admin interface of CV
---
 AKModel/admin.py |  2 +-
 AKModel/views.py | 52 ++++++++++++++++++++++++++++++++----------------
 2 files changed, 36 insertions(+), 18 deletions(-)

diff --git a/AKModel/admin.py b/AKModel/admin.py
index 1bc4a653..b26c9d63 100644
--- a/AKModel/admin.py
+++ b/AKModel/admin.py
@@ -349,7 +349,7 @@ class ConstraintViolationAdminForm(forms.ModelForm):
 
 @admin.register(ConstraintViolation)
 class ConstraintViolationAdmin(admin.ModelAdmin):
-    list_display = ['type', 'level', 'get_details']
+    list_display = ['type', 'level', 'get_details', 'manually_resolved']
     list_filter = ['event']
     readonly_fields = ['timestamp']
     form = ConstraintViolationAdminForm
diff --git a/AKModel/views.py b/AKModel/views.py
index a766d896..ea9435af 100644
--- a/AKModel/views.py
+++ b/AKModel/views.py
@@ -376,6 +376,7 @@ class ExportSlidesView(EventSlugMixin, IntermediateAdminView):
 
 class IntermediateAdminActionView(IntermediateAdminView, ABC):
     form_class = AdminIntermediateActionForm
+    entities = None
 
     def get_queryset(self, pks=None):
         if pks is None:
@@ -388,34 +389,51 @@ class IntermediateAdminActionView(IntermediateAdminView, ABC):
         return initial
 
     def get_preview(self):
-        entities = self.get_queryset()
-        joined_entities = '\n'.join(str(e) for e in entities)
+        self.entities = self.get_queryset()
+        joined_entities = '\n'.join(str(e) for e in self.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):
+    def action(self, form):
         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()
+        self.entities = self.get_queryset(pks=form.cleaned_data['pks'])
+        self.action(form)
         messages.add_message(self.request, messages.SUCCESS, self.success_message)
         return super().form_valid(form)
 
 
+class LoopActionMixin(ABC):
+    def action(self, form):
+        self.pre_action()
+        for entity in self.entities:
+            self.perform_action(entity)
+            entity.save()
+        self.post_action()
+
+    @abstractmethod
+    def perform_action(self, entity):
+        pass
+
+    def pre_action(self):
+        pass
+
+    def post_action(self):
+        pass
+
+
 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
+    def action(self, form):
+        self.entities.update(manually_resolved=True)
 
 
 class CVSetLevelViolationView(IntermediateAdminActionView):
@@ -424,8 +442,8 @@ class CVSetLevelViolationView(IntermediateAdminActionView):
     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
+    def action(self, form):
+        self.entities.update(level=ConstraintViolation.ViolationLevel.VIOLATION)
 
 
 class CVSetLevelWarningView(IntermediateAdminActionView):
@@ -434,8 +452,8 @@ class CVSetLevelWarningView(IntermediateAdminActionView):
     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
+    def action(self, form):
+        self.entities.update(level=ConstraintViolation.ViolationLevel.WARNING)
 
 
 class AKResetInterestView(IntermediateAdminActionView):
@@ -444,8 +462,8 @@ class AKResetInterestView(IntermediateAdminActionView):
     confirmation_message = _("Interest of the following AKs will be set to not filled (-1):")
     success_message = _("Reset of interest in AKs successful.")
 
-    def perform_action(self, entity):
-        entity.interest = -1
+    def action(self, form):
+        self.entities.update(interest=-1)
 
 
 class AKResetInterestCounterView(IntermediateAdminActionView):
@@ -454,5 +472,5 @@ class AKResetInterestCounterView(IntermediateAdminActionView):
     confirmation_message = _("Interest counter of the following AKs will be set to 0:")
     success_message = _("AKs' interest counters set back to 0.")
 
-    def perform_action(self, entity):
-        entity.interest_counter = 0
+    def action(self, form):
+        self.entities.update(interest_counter=0)
-- 
GitLab


From f444f3a630f2c04e0ab7556249bd95afc77bb53e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?=
 <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de>
Date: Mon, 24 Oct 2022 00:22:58 +0200
Subject: [PATCH 8/8] Introduce intermediate page for plan publishing

Use action with intermediate page instead of direct action for publishing and unpublishing of plans
This allows to create a link on the events detail page and on the status page to change the plans visibility
Add link to detail view
Visualize plan visibility on status page and allow to toggle it from there
This implements the final function of #159
---
 AKModel/admin.py                            |  27 +-
 AKModel/locale/de_DE/LC_MESSAGES/django.po  | 432 ++++++++++----------
 AKModel/templates/admin/AKModel/status.html |   7 +
 AKModel/views.py                            |  21 +
 4 files changed, 272 insertions(+), 215 deletions(-)

diff --git a/AKModel/admin.py b/AKModel/admin.py
index b26c9d63..6bbbee0f 100644
--- a/AKModel/admin.py
+++ b/AKModel/admin.py
@@ -3,7 +3,6 @@ from django.apps import apps
 from django.contrib import admin, messages
 from django.contrib.admin import SimpleListFilter, RelatedFieldListFilter, action, display
 from django.db.models import Count, F
-from django.db.models.functions import Now
 from django.http import HttpResponseRedirect
 from django.shortcuts import render, redirect
 from django.urls import reverse_lazy, path
@@ -20,7 +19,7 @@ from AKModel.models import Event, AKOwner, AKCategory, AKTrack, AKTag, AKRequire
     ConstraintViolation
 from AKModel.urls import get_admin_urls_event_wizard, get_admin_urls_event
 from AKModel.views import CVMarkResolvedView, CVSetLevelViolationView, CVSetLevelWarningView, AKResetInterestView, \
-    AKResetInterestCounterView
+    AKResetInterestCounterView, PlanPublishView, PlanUnpublishView
 
 
 class EventRelatedFieldListFilter(RelatedFieldListFilter):
@@ -39,7 +38,7 @@ class EventAdmin(admin.ModelAdmin):
     list_filter = ['active']
     list_editable = ['active']
     ordering = ['-start']
-    readonly_fields = ['status_url', 'plan_hidden', 'plan_published_at']
+    readonly_fields = ['status_url', 'plan_hidden', 'plan_published_at', 'toggle_plan_visibility']
     actions = ['publish', 'unpublish']
 
     def add_view(self, request, form_url='', extra_context=None):
@@ -53,6 +52,10 @@ class EventAdmin(admin.ModelAdmin):
         if apps.is_installed("AKScheduling"):
             from AKScheduling.urls import get_admin_urls_scheduling
             urls.extend(get_admin_urls_scheduling(self.admin_site))
+        urls.extend([
+            path('plan/publish/', PlanPublishView.as_view(), name="plan-publish"),
+            path('plan/unpublish/', PlanUnpublishView.as_view(), name="plan-unpublish"),
+        ])
         urls.extend(super().get_urls())
         return urls
 
@@ -61,6 +64,16 @@ class EventAdmin(admin.ModelAdmin):
         return format_html("<a href='{url}'>{text}</a>",
                            url=reverse_lazy('admin:event_status', kwargs={'slug': obj.slug}), text=_("Status"))
 
+    @display(description=_("Toggle plan visibility"))
+    def toggle_plan_visibility(self, obj):
+        if obj.plan_hidden:
+            url = f"{reverse_lazy('admin:plan-publish')}?pks={obj.pk}"
+            text = _('Publish plan')
+        else:
+            url = f"{reverse_lazy('admin:plan-unpublish')}?pks={obj.pk}"
+            text = _('Unpublish plan')
+        return format_html("<a href='{url}'>{text}</a>", url=url, text=text)
+
     def get_form(self, request, obj=None, change=False, **kwargs):
         # Use timezone of event
         timezone.activate(obj.timezone)
@@ -68,13 +81,13 @@ class EventAdmin(admin.ModelAdmin):
 
     @action(description=_('Publish plan'))
     def publish(self, request, queryset):
-        queryset.update(plan_published_at=Now(), plan_hidden=False)
-        self.message_user(request, _('Plan published'), messages.SUCCESS)
+        selected = queryset.values_list('pk', flat=True)
+        return HttpResponseRedirect(f"{reverse_lazy('admin:plan-publish')}?pks={','.join(str(pk) for pk in selected)}")
 
     @action(description=_('Unpublish plan'))
     def unpublish(self, request, queryset):
-        queryset.update(plan_published_at=None, plan_hidden=True)
-        self.message_user(request, _('Plan unpublished'), messages.SUCCESS)
+        selected = queryset.values_list('pk', flat=True)
+        return HttpResponseRedirect(f"{reverse_lazy('admin:plan-unpublish')}?pks={','.join(str(pk) for pk in selected)}")
 
 
 @admin.register(AKOwner)
diff --git a/AKModel/locale/de_DE/LC_MESSAGES/django.po b/AKModel/locale/de_DE/LC_MESSAGES/django.po
index ea136ff1..08531387 100644
--- a/AKModel/locale/de_DE/LC_MESSAGES/django.po
+++ b/AKModel/locale/de_DE/LC_MESSAGES/django.po
@@ -2,7 +2,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2022-10-23 23:19+0200\n"
+"POT-Creation-Date: 2022-10-24 00:20+0200\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -11,7 +11,7 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: AKModel/admin.py:59 AKModel/admin.py:62
+#: AKModel/admin.py:62 AKModel/admin.py:65
 #: AKModel/templates/admin/AKModel/event_wizard/activate.html:32
 #: AKModel/templates/admin/AKModel/event_wizard/created_prepare_import.html:48
 #: AKModel/templates/admin/AKModel/event_wizard/finish.html:21
@@ -21,63 +21,59 @@ msgstr ""
 msgid "Status"
 msgstr "Status"
 
-#: AKModel/admin.py:69
+#: AKModel/admin.py:67
+msgid "Toggle plan visibility"
+msgstr "Plansichtbarkeit ändern"
+
+#: AKModel/admin.py:71 AKModel/admin.py:82 AKModel/views.py:481
 msgid "Publish plan"
 msgstr "Plan veröffentlichen"
 
-#: AKModel/admin.py:72
-msgid "Plan published"
-msgstr "Plan veröffentlicht"
-
-#: AKModel/admin.py:74
+#: AKModel/admin.py:74 AKModel/admin.py:87 AKModel/views.py:491
 msgid "Unpublish plan"
 msgstr "Plan verbergen"
 
-#: AKModel/admin.py:77
-msgid "Plan unpublished"
-msgstr "Plan verborgen"
-
-#: AKModel/admin.py:146
+#: AKModel/admin.py:159
 msgid "Wish"
 msgstr "AK-Wunsch"
 
-#: AKModel/admin.py:152
+#: AKModel/admin.py:165
 msgid "Is wish"
 msgstr "Ist ein Wunsch"
 
-#: AKModel/admin.py:153
+#: AKModel/admin.py:166
 msgid "Is not a wish"
 msgstr "Ist kein Wunsch"
 
-#: AKModel/admin.py:197
+#: AKModel/admin.py:210
 msgid "Export to wiki syntax"
 msgstr "In Wiki-Syntax exportieren"
 
-#: AKModel/admin.py:205
+#: AKModel/admin.py:219
 msgid "Cannot export AKs from more than one event at the same time."
 msgstr "Kann nicht AKs von mehreren Events zur selben Zeit exportieren."
 
-#: AKModel/admin.py:220 AKModel/views.py:442
+#: AKModel/admin.py:234 AKModel/views.py:461
 msgid "Reset interest in AKs"
 msgstr "Interesse an AKs zurücksetzen"
 
-#: AKModel/admin.py:225 AKModel/views.py:452
+#: AKModel/admin.py:239 AKModel/views.py:471
 msgid "Reset AKs' interest counters"
 msgstr "Interessenszähler der AKs zurücksetzen"
 
-#: AKModel/admin.py:309
+#: AKModel/admin.py:323
 msgid "AK Details"
 msgstr "AK-Details"
 
-#: AKModel/admin.py:366 AKModel/views.py:412
+#: AKModel/admin.py:380 AKModel/views.py:431
 msgid "Mark Constraint Violations as manually resolved"
 msgstr "Markiere Constraintverletzungen als manuell behoben"
 
-#: AKModel/admin.py:371 AKModel/views.py:422
+#: AKModel/admin.py:385 AKModel/views.py:441
 msgid "Set Constraint Violations to level \"violation\""
 msgstr "Constraintverletzungen auf Level \"Violation\" setzen"
 
-#: AKModel/admin.py:376 AKModel/views.py:432
+#: AKModel/admin.py:390 AKModel/views.py:451
 msgid "Set Constraint Violations to level \"warning\""
 msgstr "Constraintverletzungen auf Level \"Warning\" setzen"
 
@@ -105,17 +101,17 @@ msgstr "Die eingegebene Verfügbarkeit enthält ein ungültiges Datum."
 msgid "Please fill in your availabilities!"
 msgstr "Bitte Verfügbarkeiten eintragen!"
 
-#: AKModel/availability/models.py:38 AKModel/models.py:56 AKModel/models.py:126
-#: AKModel/models.py:181 AKModel/models.py:200 AKModel/models.py:232
-#: AKModel/models.py:286 AKModel/models.py:352 AKModel/models.py:385
-#: AKModel/models.py:456 AKModel/models.py:497
+#: AKModel/availability/models.py:38 AKModel/models.py:56 AKModel/models.py:128
+#: AKModel/models.py:183 AKModel/models.py:202 AKModel/models.py:234
+#: AKModel/models.py:288 AKModel/models.py:354 AKModel/models.py:387
+#: AKModel/models.py:458 AKModel/models.py:499
 msgid "Event"
 msgstr "Event"
 
-#: AKModel/availability/models.py:39 AKModel/models.py:127
-#: AKModel/models.py:182 AKModel/models.py:201 AKModel/models.py:233
-#: AKModel/models.py:287 AKModel/models.py:353 AKModel/models.py:386
-#: AKModel/models.py:457 AKModel/models.py:498
+#: AKModel/availability/models.py:39 AKModel/models.py:129
+#: AKModel/models.py:184 AKModel/models.py:203 AKModel/models.py:235
+#: AKModel/models.py:289 AKModel/models.py:355 AKModel/models.py:388
+#: AKModel/models.py:459 AKModel/models.py:500
 msgid "Associated event"
 msgstr "Zugehöriges Event"
 
@@ -127,8 +123,8 @@ msgstr "Person"
 msgid "Person whose availability this is"
 msgstr "Person deren Verfügbarkeit hier abgebildet wird"
 
-#: AKModel/availability/models.py:56 AKModel/models.py:356
-#: AKModel/models.py:375 AKModel/models.py:506
+#: AKModel/availability/models.py:56 AKModel/models.py:358
+#: AKModel/models.py:377 AKModel/models.py:508
 msgid "Room"
 msgstr "Raum"
 
@@ -136,8 +132,8 @@ msgstr "Raum"
 msgid "Room whose availability this is"
 msgstr "Raum dessen Verfügbarkeit hier abgebildet wird"
 
-#: AKModel/availability/models.py:65 AKModel/models.py:292
-#: AKModel/models.py:374 AKModel/models.py:451
+#: AKModel/availability/models.py:65 AKModel/models.py:294
+#: AKModel/models.py:376 AKModel/models.py:453
 msgid "AK"
 msgstr "AK"
 
@@ -145,8 +141,8 @@ msgstr "AK"
 msgid "AK whose availability this is"
 msgstr "Verfügbarkeiten"
 
-#: AKModel/availability/models.py:74 AKModel/models.py:185
-#: AKModel/models.py:512
+#: AKModel/availability/models.py:74 AKModel/models.py:187
+#: AKModel/models.py:514
 msgid "AK Category"
 msgstr "AK-Kategorie"
 
@@ -212,9 +208,9 @@ msgstr ""
 "fürWünsche markieren, z.B. um während der Präsentation auf einem Touchscreen "
 "ausgefüllt zu werden?"
 
-#: AKModel/models.py:18 AKModel/models.py:173 AKModel/models.py:197
-#: AKModel/models.py:216 AKModel/models.py:230 AKModel/models.py:248
-#: AKModel/models.py:344
+#: AKModel/models.py:18 AKModel/models.py:175 AKModel/models.py:199
+#: AKModel/models.py:218 AKModel/models.py:232 AKModel/models.py:250
+#: AKModel/models.py:346
 msgid "Name"
 msgstr "Name"
 
@@ -248,7 +244,7 @@ msgstr "Zeitzone"
 msgid "Time Zone where this event takes place in"
 msgstr "Zeitzone in der das Event stattfindet"
 
-#: AKModel/models.py:27 AKModel/views.py:241
+#: AKModel/models.py:27 AKModel/views.py:242
 msgid "Start"
 msgstr "Start"
 
@@ -356,71 +352,71 @@ msgstr ""
 msgid "Events"
 msgstr "Events"
 
-#: AKModel/models.py:121
+#: AKModel/models.py:123
 msgid "Nickname"
 msgstr "Spitzname"
 
-#: AKModel/models.py:121
+#: AKModel/models.py:123
 msgid "Name to identify an AK owner by"
 msgstr "Name, durch den eine AK-Leitung identifiziert wird"
 
-#: AKModel/models.py:122
+#: AKModel/models.py:124
 msgid "Slug"
 msgstr "Slug"
 
-#: AKModel/models.py:122
+#: AKModel/models.py:124
 msgid "Slug for URL generation"
 msgstr "Slug für URL-Generierung"
 
-#: AKModel/models.py:123
+#: AKModel/models.py:125
 msgid "Institution"
 msgstr "Instutution"
 
-#: AKModel/models.py:123
+#: AKModel/models.py:125
 msgid "Uni etc."
 msgstr "Universität o.ä."
 
-#: AKModel/models.py:124 AKModel/models.py:257
+#: AKModel/models.py:126 AKModel/models.py:259
 msgid "Web Link"
 msgstr "Internet Link"
 
-#: AKModel/models.py:124
+#: AKModel/models.py:126
 msgid "Link to Homepage"
 msgstr "Link zu Homepage oder Webseite"
 
-#: AKModel/models.py:130 AKModel/models.py:505
+#: AKModel/models.py:132 AKModel/models.py:507
 msgid "AK Owner"
 msgstr "AK-Leitung"
 
-#: AKModel/models.py:131
+#: AKModel/models.py:133
 msgid "AK Owners"
 msgstr "AK-Leitungen"
 
-#: AKModel/models.py:173
+#: AKModel/models.py:175
 msgid "Name of the AK Category"
 msgstr "Name der AK-Kategorie"
 
-#: AKModel/models.py:174 AKModel/models.py:198
+#: AKModel/models.py:176 AKModel/models.py:200
 msgid "Color"
 msgstr "Farbe"
 
-#: AKModel/models.py:174 AKModel/models.py:198
+#: AKModel/models.py:176 AKModel/models.py:200
 msgid "Color for displaying"
 msgstr "Farbe für die Anzeige"
 
-#: AKModel/models.py:175 AKModel/models.py:251
+#: AKModel/models.py:177 AKModel/models.py:253
 msgid "Description"
 msgstr "Beschreibung"
 
-#: AKModel/models.py:176
+#: AKModel/models.py:178
 msgid "Short description of this AK Category"
 msgstr "Beschreibung der AK-Kategorie"
 
-#: AKModel/models.py:177
+#: AKModel/models.py:179
 msgid "Present by default"
 msgstr "Defaultmäßig präsentieren"
 
-#: AKModel/models.py:179
+#: AKModel/models.py:181
 msgid ""
 "Present AKs of this category by default if AK owner did not specify whether "
 "this AK should be presented?"
@@ -428,152 +424,152 @@ msgstr ""
 "AKs dieser Kategorie standardmäßig vorstellen, wenn die Leitungen das für "
 "ihren AK nicht explizit spezifiziert haben?"
 
-#: AKModel/models.py:186
+#: AKModel/models.py:188
 msgid "AK Categories"
 msgstr "AK-Kategorien"
 
-#: AKModel/models.py:197
+#: AKModel/models.py:199
 msgid "Name of the AK Track"
 msgstr "Name des AK-Tracks"
 
-#: AKModel/models.py:204
+#: AKModel/models.py:206
 msgid "AK Track"
 msgstr "AK-Track"
 
-#: AKModel/models.py:205
+#: AKModel/models.py:207
 msgid "AK Tracks"
 msgstr "AK-Tracks"
 
-#: AKModel/models.py:216
+#: AKModel/models.py:218
 msgid "Name of the AK Tag"
 msgstr "Name das AK-Tags"
 
-#: AKModel/models.py:219
+#: AKModel/models.py:221
 msgid "AK Tag"
 msgstr "AK-Tag"
 
-#: AKModel/models.py:220
+#: AKModel/models.py:222
 msgid "AK Tags"
 msgstr "AK-Tags"
 
-#: AKModel/models.py:230
+#: AKModel/models.py:232
 msgid "Name of the Requirement"
 msgstr "Name der Anforderung"
 
-#: AKModel/models.py:236 AKModel/models.py:509
+#: AKModel/models.py:238 AKModel/models.py:511
 msgid "AK Requirement"
 msgstr "AK-Anforderung"
 
-#: AKModel/models.py:237
+#: AKModel/models.py:239
 msgid "AK Requirements"
 msgstr "AK-Anforderungen"
 
-#: AKModel/models.py:248
+#: AKModel/models.py:250
 msgid "Name of the AK"
 msgstr "Name des AKs"
 
-#: AKModel/models.py:249
+#: AKModel/models.py:251
 msgid "Short Name"
 msgstr "Kurzer Name"
 
-#: AKModel/models.py:250
+#: AKModel/models.py:252
 msgid "Name displayed in the schedule"
 msgstr "Name zur Anzeige im AK-Plan"
 
-#: AKModel/models.py:251
+#: AKModel/models.py:253
 msgid "Description of the AK"
 msgstr "Beschreibung des AKs"
 
-#: AKModel/models.py:253
+#: AKModel/models.py:255
 msgid "Owners"
 msgstr "Leitungen"
 
-#: AKModel/models.py:254
+#: AKModel/models.py:256
 msgid "Those organizing the AK"
 msgstr "Menschen, die den AK organisieren und halten"
 
-#: AKModel/models.py:257
+#: AKModel/models.py:259
 msgid "Link to wiki page"
 msgstr "Link zur Wiki Seite"
 
-#: AKModel/models.py:258
+#: AKModel/models.py:260
 msgid "Protocol Link"
 msgstr "Protokolllink"
 
-#: AKModel/models.py:258
+#: AKModel/models.py:260
 msgid "Link to protocol"
 msgstr "Link zum Protokoll"
 
-#: AKModel/models.py:260
+#: AKModel/models.py:262
 msgid "Category"
 msgstr "Kategorie"
 
-#: AKModel/models.py:261
+#: AKModel/models.py:263
 msgid "Category of the AK"
 msgstr "Kategorie des AKs"
 
-#: AKModel/models.py:262
+#: AKModel/models.py:264
 msgid "Tags"
 msgstr "Tags"
 
-#: AKModel/models.py:262
+#: AKModel/models.py:264
 msgid "Tags provided by owners"
 msgstr "Tags, die durch die AK-Leitung vergeben wurden"
 
-#: AKModel/models.py:263
+#: AKModel/models.py:265
 msgid "Track"
 msgstr "Track"
 
-#: AKModel/models.py:264
+#: AKModel/models.py:266
 msgid "Track the AK belongs to"
 msgstr "Track zu dem der AK gehört"
 
-#: AKModel/models.py:266
+#: AKModel/models.py:268
 msgid "Resolution Intention"
 msgstr "Resolutionsabsicht"
 
-#: AKModel/models.py:267
+#: AKModel/models.py:269
 msgid "Intends to submit a resolution"
 msgstr "Beabsichtigt eine Resolution einzureichen"
 
-#: AKModel/models.py:268
+#: AKModel/models.py:270
 msgid "Present this AK"
 msgstr "AK präsentieren"
 
-#: AKModel/models.py:269
+#: AKModel/models.py:271
 msgid "Present results of this AK"
 msgstr "Die Ergebnisse dieses AKs vorstellen"
 
-#: AKModel/models.py:271 AKModel/templates/admin/AKModel/status.html:95
+#: AKModel/models.py:273 AKModel/templates/admin/AKModel/status.html:102
 msgid "Requirements"
 msgstr "Anforderungen"
 
-#: AKModel/models.py:272
+#: AKModel/models.py:274
 msgid "AK's Requirements"
 msgstr "Anforderungen des AKs"
 
-#: AKModel/models.py:274
+#: AKModel/models.py:276
 msgid "Conflicting AKs"
 msgstr "AK-Konflikte"
 
-#: AKModel/models.py:275
+#: AKModel/models.py:277
 msgid "AKs that conflict and thus must not take place at the same time"
 msgstr ""
 "AKs, die Konflikte haben und deshalb nicht gleichzeitig stattfinden dürfen"
 
-#: AKModel/models.py:276
+#: AKModel/models.py:278
 msgid "Prerequisite AKs"
 msgstr "Vorausgesetzte AKs"
 
-#: AKModel/models.py:277
+#: AKModel/models.py:279
 msgid "AKs that should precede this AK in the schedule"
 msgstr "AKs die im AK-Plan vor diesem AK stattfinden müssen"
 
-#: AKModel/models.py:279
+#: AKModel/models.py:281
 msgid "Organizational Notes"
 msgstr "Notizen zur Organisation"
 
-#: AKModel/models.py:280
+#: AKModel/models.py:282
 msgid ""
 "Notes to organizers. These are public. For private notes, please use the "
 "button for private messages on the detail page of this AK (after creation/"
@@ -583,258 +579,258 @@ msgstr ""
 "Anmerkungen bitte den Button für Direktnachrichten verwenden (nach dem "
 "Anlegen/Bearbeiten)."
 
-#: AKModel/models.py:282
+#: AKModel/models.py:284
 msgid "Interest"
 msgstr "Interesse"
 
-#: AKModel/models.py:282
+#: AKModel/models.py:284
 msgid "Expected number of people"
 msgstr "Erwartete Personenzahl"
 
-#: AKModel/models.py:283
+#: AKModel/models.py:285
 msgid "Interest Counter"
 msgstr "Interessenszähler"
 
-#: AKModel/models.py:284
+#: AKModel/models.py:286
 msgid "People who have indicated interest online"
 msgstr "Anzahl Personen, die online Interesse bekundet haben"
 
-#: AKModel/models.py:293 AKModel/models.py:500
-#: AKModel/templates/admin/AKModel/status.html:49
-#: AKModel/templates/admin/AKModel/status.html:56 AKModel/views.py:359
+#: AKModel/models.py:295 AKModel/models.py:502
+#: AKModel/templates/admin/AKModel/status.html:56
+#: AKModel/templates/admin/AKModel/status.html:63 AKModel/views.py:360
 msgid "AKs"
 msgstr "AKs"
 
-#: AKModel/models.py:344
+#: AKModel/models.py:346
 msgid "Name or number of the room"
 msgstr "Name oder Nummer des Raums"
 
-#: AKModel/models.py:345
+#: AKModel/models.py:347
 msgid "Location"
 msgstr "Ort"
 
-#: AKModel/models.py:346
+#: AKModel/models.py:348
 msgid "Name or number of the location"
 msgstr "Name oder Nummer des Ortes"
 
-#: AKModel/models.py:347
+#: AKModel/models.py:349
 msgid "Capacity"
 msgstr "Kapazität"
 
-#: AKModel/models.py:348
+#: AKModel/models.py:350
 msgid "Maximum number of people (-1 for unlimited)."
 msgstr "Maximale Personenzahl (-1 wenn unbeschränkt)."
 
-#: AKModel/models.py:349
+#: AKModel/models.py:351
 msgid "Properties"
 msgstr "Eigenschaften"
 
-#: AKModel/models.py:350
+#: AKModel/models.py:352
 msgid "AK requirements fulfilled by the room"
 msgstr "AK-Anforderungen, die dieser Raum erfüllt"
 
-#: AKModel/models.py:357 AKModel/templates/admin/AKModel/status.html:33
+#: AKModel/models.py:359 AKModel/templates/admin/AKModel/status.html:40
 msgid "Rooms"
 msgstr "Räume"
 
-#: AKModel/models.py:374
+#: AKModel/models.py:376
 msgid "AK being mapped"
 msgstr "AK, der zugeordnet wird"
 
-#: AKModel/models.py:376
+#: AKModel/models.py:378
 msgid "Room the AK will take place in"
 msgstr "Raum in dem der AK stattfindet"
 
-#: AKModel/models.py:377
+#: AKModel/models.py:379
 msgid "Slot Begin"
 msgstr "Beginn des Slots"
 
-#: AKModel/models.py:377
+#: AKModel/models.py:379
 msgid "Time and date the slot begins"
 msgstr "Zeit und Datum zu der der AK beginnt"
 
-#: AKModel/models.py:379
+#: AKModel/models.py:381
 msgid "Duration"
 msgstr "Dauer"
 
-#: AKModel/models.py:380
+#: AKModel/models.py:382
 msgid "Length in hours"
 msgstr "Länge in Stunden"
 
-#: AKModel/models.py:382
+#: AKModel/models.py:384
 msgid "Scheduling fixed"
 msgstr "Planung fix"
 
-#: AKModel/models.py:383
+#: AKModel/models.py:385
 msgid "Length and time of this AK should not be changed"
 msgstr "Dauer und Zeit dieses AKs sollten nicht verändert werden"
 
-#: AKModel/models.py:388
+#: AKModel/models.py:390
 msgid "Last update"
 msgstr "Letzte Aktualisierung"
 
-#: AKModel/models.py:391
+#: AKModel/models.py:393
 msgid "AK Slot"
 msgstr "AK-Slot"
 
-#: AKModel/models.py:392 AKModel/models.py:502
+#: AKModel/models.py:394 AKModel/models.py:504
 msgid "AK Slots"
 msgstr "AK-Slot"
 
-#: AKModel/models.py:414 AKModel/models.py:423
+#: AKModel/models.py:416 AKModel/models.py:425
 msgid "Not scheduled yet"
 msgstr "Noch nicht geplant"
 
-#: AKModel/models.py:452
+#: AKModel/models.py:454
 msgid "AK this message belongs to"
 msgstr "AK zu dem die Nachricht gehört"
 
-#: AKModel/models.py:453
+#: AKModel/models.py:455
 msgid "Message text"
 msgstr "Nachrichtentext"
 
-#: AKModel/models.py:454
+#: AKModel/models.py:456
 msgid "Message to the organizers. This is not publicly visible."
 msgstr ""
 "Nachricht an die Organisator*innen. Diese ist nicht öffentlich sichtbar."
 
-#: AKModel/models.py:460
+#: AKModel/models.py:462
 msgid "AK Orga Message"
 msgstr "AK-Organachricht"
 
-#: AKModel/models.py:461
+#: AKModel/models.py:463
 msgid "AK Orga Messages"
 msgstr "AK-Organachrichten"
 
-#: AKModel/models.py:470
+#: AKModel/models.py:472
 msgid "Constraint Violation"
 msgstr "Constraintverletzung"
 
-#: AKModel/models.py:471 AKModel/templates/admin/AKModel/status.html:79
+#: AKModel/models.py:473 AKModel/templates/admin/AKModel/status.html:86
 msgid "Constraint Violations"
 msgstr "Constraintverletzungen"
 
-#: AKModel/models.py:475
+#: AKModel/models.py:477
 msgid "Owner has two parallel slots"
 msgstr "Leitung hat zwei Slots parallel"
 
-#: AKModel/models.py:476
+#: AKModel/models.py:478
 msgid "AK Slot was scheduled outside the AK's availabilities"
 msgstr "AK Slot wurde außerhalb der Verfügbarkeit des AKs platziert"
 
-#: AKModel/models.py:477
+#: AKModel/models.py:479
 msgid "Room has two AK slots scheduled at the same time"
 msgstr "Raum hat zwei AK Slots gleichzeitig"
 
-#: AKModel/models.py:478
+#: AKModel/models.py:480
 msgid "Room does not satisfy the requirement of the scheduled AK"
 msgstr "Room erfüllt die Anforderungen des platzierten AKs nicht"
 
-#: AKModel/models.py:479
+#: AKModel/models.py:481
 msgid "AK Slot is scheduled at the same time as an AK listed as a conflict"
 msgstr ""
 "AK Slot wurde wurde zur gleichen Zeit wie ein Konflikt des AKs platziert"
 
-#: AKModel/models.py:480
+#: AKModel/models.py:482
 msgid "AK Slot is scheduled before an AK listed as a prerequisite"
 msgstr "AK Slot wurde vor einem als Voraussetzung gelisteten AK platziert"
 
-#: AKModel/models.py:482
+#: AKModel/models.py:484
 msgid ""
 "AK Slot for AK with intention to submit a resolution is scheduled after "
 "resolution deadline"
 msgstr ""
 "AK Slot eines AKs mit Resoabsicht wurde nach der Resodeadline platziert"
 
-#: AKModel/models.py:483
+#: AKModel/models.py:485
 msgid "AK Slot in a category is outside that categories availabilities"
 msgstr "AK Slot wurde außerhalb der Verfügbarkeiten seiner Kategorie"
 
-#: AKModel/models.py:484
+#: AKModel/models.py:486
 msgid "Two AK Slots for the same AK scheduled at the same time"
 msgstr "Zwei AK Slots eines AKs wurden zur selben Zeit platziert"
 
-#: AKModel/models.py:485
+#: AKModel/models.py:487
 msgid "Room does not have enough space for interest in scheduled AK Slot"
 msgstr "Room hat nicht genug Platz für das Interesse am geplanten AK-Slot"
 
-#: AKModel/models.py:486
+#: AKModel/models.py:488
 msgid "AK Slot is scheduled outside the event's availabilities"
 msgstr "AK Slot wurde außerhalb der Verfügbarkeit des Events platziert"
 
-#: AKModel/models.py:489
+#: AKModel/models.py:491
 msgid "Warning"
 msgstr "Warnung"
 
-#: AKModel/models.py:490
+#: AKModel/models.py:492
 msgid "Violation"
 msgstr "Verletzung"
 
-#: AKModel/models.py:492
+#: AKModel/models.py:494
 msgid "Type"
 msgstr "Art"
 
-#: AKModel/models.py:493
+#: AKModel/models.py:495
 msgid "Type of violation, i.e. what kind of constraint was violated"
 msgstr "Art der Verletzung, gibt an welche Art Constraint verletzt wurde"
 
-#: AKModel/models.py:494
+#: AKModel/models.py:496
 msgid "Level"
 msgstr "Level"
 
-#: AKModel/models.py:495
+#: AKModel/models.py:497
 msgid "Severity level of the violation"
 msgstr "Schweregrad der Verletzung"
 
-#: AKModel/models.py:501
+#: AKModel/models.py:503
 msgid "AK(s) belonging to this constraint"
 msgstr "AK(s), die zu diesem Constraint gehören"
 
-#: AKModel/models.py:503
+#: AKModel/models.py:505
 msgid "AK Slot(s) belonging to this constraint"
 msgstr "AK Slot(s), die zu diesem Constraint gehören"
 
-#: AKModel/models.py:505
+#: AKModel/models.py:507
 msgid "AK Owner belonging to this constraint"
 msgstr "AK Leitung(en), die zu diesem Constraint gehören"
 
-#: AKModel/models.py:507
+#: AKModel/models.py:509
 msgid "Room belonging to this constraint"
 msgstr "Raum, der zu diesem Constraint gehört"
 
-#: AKModel/models.py:510
+#: AKModel/models.py:512
 msgid "AK Requirement belonging to this constraint"
 msgstr "AK Anforderung, die zu diesem Constraint gehört"
 
-#: AKModel/models.py:512
+#: AKModel/models.py:514
 msgid "AK Category belonging to this constraint"
 msgstr "AK Kategorie, di zu diesem Constraint gehört"
 
-#: AKModel/models.py:514
+#: AKModel/models.py:516
 msgid "Comment"
 msgstr "Kommentar"
 
-#: AKModel/models.py:514
+#: AKModel/models.py:516
 msgid "Comment or further details for this violation"
 msgstr "Kommentar oder weitere Details zu dieser Vereletzung"
 
-#: AKModel/models.py:517
+#: AKModel/models.py:519
 msgid "Timestamp"
 msgstr "Timestamp"
 
-#: AKModel/models.py:517
+#: AKModel/models.py:519
 msgid "Time of creation"
 msgstr "Zeitpunkt der ERstellung"
 
-#: AKModel/models.py:518
+#: AKModel/models.py:520
 msgid "Manually Resolved"
 msgstr "Manuell behoben"
 
-#: AKModel/models.py:519
+#: AKModel/models.py:521
 msgid "Mark this violation manually as resolved"
 msgstr "Markiere diese Verletzung manuell als behoben"
 
-#: AKModel/models.py:546
+#: AKModel/models.py:548
 #: AKModel/templates/admin/AKModel/requirements_overview.html:27
 msgid "Details"
 msgstr "Details"
@@ -886,7 +882,7 @@ msgid "Successfully imported.<br><br>Do you want to activate your event now?"
 msgstr "Erfolgreich importiert.<br><br>Soll das Event jetzt aktiviert werden?"
 
 #: AKModel/templates/admin/AKModel/event_wizard/activate.html:27
-#: AKModel/views.py:246
+#: AKModel/views.py:247
 msgid "Finish"
 msgstr "Abschluss"
 
@@ -951,84 +947,88 @@ msgid "No AKs with this requirement"
 msgstr "Kein AK mit dieser Anforderung"
 
 #: AKModel/templates/admin/AKModel/requirements_overview.html:45
-#: AKModel/templates/admin/AKModel/status.html:111
+#: AKModel/templates/admin/AKModel/status.html:118
 msgid "Add Requirement"
 msgstr "Anforderung hinzufügen"
 
-#: AKModel/templates/admin/AKModel/status.html:16
+#: AKModel/templates/admin/AKModel/status.html:18
+msgid "Plan published?"
+msgstr "Plan veröffentlicht?"
+
+#: AKModel/templates/admin/AKModel/status.html:23
 msgid "Categories"
 msgstr "Kategorien"
 
-#: AKModel/templates/admin/AKModel/status.html:18
+#: AKModel/templates/admin/AKModel/status.html:25
 msgid "No categories yet"
 msgstr "Bisher keine Kategorien"
 
-#: AKModel/templates/admin/AKModel/status.html:31
+#: AKModel/templates/admin/AKModel/status.html:38
 msgid "Add category"
 msgstr "Kategorie hinzufügen"
 
-#: AKModel/templates/admin/AKModel/status.html:35
+#: AKModel/templates/admin/AKModel/status.html:42
 msgid "No rooms yet"
 msgstr "Bisher keine Räume"
 
-#: AKModel/templates/admin/AKModel/status.html:47
+#: AKModel/templates/admin/AKModel/status.html:54
 msgid "Add Room"
 msgstr "Raum hinzufügen"
 
-#: AKModel/templates/admin/AKModel/status.html:51
+#: AKModel/templates/admin/AKModel/status.html:58
 msgid "No AKs yet"
 msgstr "Bisher keine AKs"
 
-#: AKModel/templates/admin/AKModel/status.html:59
+#: AKModel/templates/admin/AKModel/status.html:66
 msgid "Slots"
 msgstr "Slots"
 
-#: AKModel/templates/admin/AKModel/status.html:62
+#: AKModel/templates/admin/AKModel/status.html:69
 msgid "Unscheduled Slots"
 msgstr "Ungeplante Slots"
 
-#: AKModel/templates/admin/AKModel/status.html:76
+#: AKModel/templates/admin/AKModel/status.html:83
 #: AKModel/templates/admin/ak_index.html:16
 msgid "Scheduling"
 msgstr "Scheduling"
 
-#: AKModel/templates/admin/AKModel/status.html:81
+#: AKModel/templates/admin/AKModel/status.html:88
 msgid "AKs requiring special attention"
 msgstr "AKs, die besondere Aufmerksamkeit benötigen"
 
-#: AKModel/templates/admin/AKModel/status.html:83
+#: AKModel/templates/admin/AKModel/status.html:90
 msgid "Enter Interest"
 msgstr "Interesse erfassen"
 
-#: AKModel/templates/admin/AKModel/status.html:86
+#: AKModel/templates/admin/AKModel/status.html:93
 msgid "Manage ak tracks"
 msgstr "AK-Tracks verwalten"
 
-#: AKModel/templates/admin/AKModel/status.html:88
+#: AKModel/templates/admin/AKModel/status.html:95
 msgid "Export AKs as CSV"
 msgstr "AKs als CSV exportieren"
 
-#: AKModel/templates/admin/AKModel/status.html:90
+#: AKModel/templates/admin/AKModel/status.html:97
 msgid "Export AKs for Wiki"
 msgstr "AKs im Wiki-Format exportieren"
 
-#: AKModel/templates/admin/AKModel/status.html:92 AKModel/views.py:329
+#: AKModel/templates/admin/AKModel/status.html:99 AKModel/views.py:330
 msgid "Export AK Slides"
 msgstr "AK-Folien exportieren"
 
-#: AKModel/templates/admin/AKModel/status.html:97
+#: AKModel/templates/admin/AKModel/status.html:104
 msgid "No requirements yet"
 msgstr "Bisher keine Anforderungen"
 
-#: AKModel/templates/admin/AKModel/status.html:110
+#: AKModel/templates/admin/AKModel/status.html:117
 msgid "Show AKs for requirements"
 msgstr "Zu Anforderungen gehörige AKs anzeigen"
 
-#: AKModel/templates/admin/AKModel/status.html:114
+#: AKModel/templates/admin/AKModel/status.html:121
 msgid "Messages"
 msgstr "Nachrichten"
 
-#: AKModel/templates/admin/AKModel/status.html:116
+#: AKModel/templates/admin/AKModel/status.html:123
 msgid "Delete all messages"
 msgstr "Alle Nachrichten löschen"
 
@@ -1065,120 +1065,136 @@ msgstr "Login"
 msgid "Register"
 msgstr "Registrieren"
 
-#: AKModel/views.py:147
+#: AKModel/views.py:148
 msgid "Event Status"
 msgstr "Eventstatus"
 
-#: AKModel/views.py:160
+#: AKModel/views.py:161
 msgid "Requirements for Event"
 msgstr "Anforderungen für das Event"
 
-#: AKModel/views.py:174
+#: AKModel/views.py:175
 msgid "AK CSV Export"
 msgstr "AK-CSV-Export"
 
-#: AKModel/views.py:188
+#: AKModel/views.py:189
 msgid "AK Wiki Export"
 msgstr "AK-Wiki-Export"
 
-#: AKModel/views.py:196 AKModel/views.py:345
+#: AKModel/views.py:197 AKModel/views.py:346
 msgid "Wishes"
 msgstr "Wünsche"
 
-#: AKModel/views.py:217
+#: AKModel/views.py:218
 msgid "Delete AK Orga Messages"
 msgstr "AK-Organachrichten löschen"
 
-#: AKModel/views.py:232
+#: AKModel/views.py:233
 msgid "AK Orga Messages successfully deleted"
 msgstr "AK-Organachrichten erfolgreich gelöscht"
 
-#: AKModel/views.py:242
+#: AKModel/views.py:243
 msgid "Settings"
 msgstr "Einstellungen"
 
-#: AKModel/views.py:243
+#: AKModel/views.py:244
 msgid "Event created, Prepare Import"
 msgstr "Event angelegt, Import vorbereiten"
 
-#: AKModel/views.py:244
+#: AKModel/views.py:245
 msgid "Import categories & requirements"
 msgstr "Kategorien & Anforderungen kopieren"
 
-#: AKModel/views.py:245
+#: AKModel/views.py:246
 msgid "Activate?"
 msgstr "Aktivieren?"
 
-#: AKModel/views.py:304
+#: AKModel/views.py:305
 #, python-format
 msgid "Copied '%(obj)s'"
 msgstr "'%(obj)s' kopiert"
 
-#: AKModel/views.py:307
+#: AKModel/views.py:308
 #, python-format
 msgid "Could not copy '%(obj)s' (%(error)s)"
 msgstr "'%(obj)s' konnte nicht kopiert werden (%(error)s)"
 
-#: AKModel/views.py:340
+#: AKModel/views.py:341
 msgid "Symbols"
 msgstr "Symbole"
 
-#: AKModel/views.py:341
+#: AKModel/views.py:342
 msgid "Who?"
 msgstr "Wer?"
 
-#: AKModel/views.py:342
+#: AKModel/views.py:343
 msgid "Duration(s)"
 msgstr "Dauer(n)"
 
-#: AKModel/views.py:343
+#: AKModel/views.py:344
 msgid "Reso intention?"
 msgstr "Resolutionsabsicht?"
 
-#: AKModel/views.py:344
+#: AKModel/views.py:345
 msgid "Category (for Wishes)"
 msgstr "Kategorie (für Wünsche)"
 
-#: AKModel/views.py:414
+#: AKModel/views.py:433
 msgid "The following Constraint Violations will be marked as manually resolved"
 msgstr ""
 "Die folgenden Constraintverletzungen werden als manuell behoben markiert."
 
-#: AKModel/views.py:415
+#: AKModel/views.py:434
 msgid "Constraint Violations marked as resolved"
 msgstr "Constraintverletzungen als manuell behoben markiert"
 
-#: AKModel/views.py:424
+#: AKModel/views.py:443
 msgid "The following Constraint Violations will be set to level 'violation'"
 msgstr ""
 "Die folgenden Constraintverletzungen werden auf das Level \"Violation\" "
 "gesetzt."
 
-#: AKModel/views.py:425
+#: AKModel/views.py:444
 msgid "Constraint Violations set to level 'violation'"
 msgstr "Constraintverletzungen auf Level \"Violation\" gesetzt"
 
-#: AKModel/views.py:434
+#: AKModel/views.py:453
 msgid "The following Constraint Violations will be set to level 'warning'"
 msgstr ""
 "Die folgenden Constraintverletzungen werden auf das Level 'warning' gesetzt."
 
-#: AKModel/views.py:435
+#: AKModel/views.py:454
 msgid "Constraint Violations set to level 'warning'"
 msgstr "Constraintverletzungen auf Level \"Warning\" gesetzt"
 
-#: AKModel/views.py:444
+#: AKModel/views.py:463
 msgid "Interest of the following AKs will be set to not filled (-1):"
 msgstr "Interesse an den folgenden AKs wird auf nicht ausgefüllt (-1) gesetzt:"
 
-#: AKModel/views.py:445
+#: AKModel/views.py:464
 msgid "Reset of interest in AKs successful."
 msgstr "Interesse an AKs erfolgreich zurückgesetzt."
 
-#: AKModel/views.py:454
+#: AKModel/views.py:473
 msgid "Interest counter of the following AKs will be set to 0:"
 msgstr "Interessensbekundungszähler der folgenden AKs wird auf 0 gesetzt:"
 
-#: AKModel/views.py:455
+#: AKModel/views.py:474
 msgid "AKs' interest counters set back to 0."
 msgstr "Interessenszähler der AKs zurückgesetzt"
+
+#: AKModel/views.py:483
+msgid "Publish the plan(s) of:"
+msgstr "Den Plan/die Pläne veröffentlichen von:"
+
+#: AKModel/views.py:484
+msgid "Plan published"
+msgstr "Plan veröffentlicht"
+
+#: AKModel/views.py:493
+msgid "Unpublish the plan(s) of:"
+msgstr "Den Plan/die Pläne verbergen von:"
+
+#: AKModel/views.py:494
+msgid "Plan unpublished"
+msgstr "Plan verborgen"
diff --git a/AKModel/templates/admin/AKModel/status.html b/AKModel/templates/admin/AKModel/status.html
index 19d753c9..d279efea 100644
--- a/AKModel/templates/admin/AKModel/status.html
+++ b/AKModel/templates/admin/AKModel/status.html
@@ -11,6 +11,13 @@
         <h2><a href="{% url 'admin:AKModel_event_change' event.pk %}">{{event}}</a></h2>
         <h5>{{ event.start }} - {{ event.end }}</h5>
 
+        <div class="custom-control custom-switch mt-2 mb-2">
+          <input type="checkbox" class="custom-control-input" id="planPublishedSwitch"
+                 {% if not event.plan_hidden %}checked{% endif %}
+                 onclick="location.href='{% if event.plan_hidden %}{% url 'admin:plan-publish' %}{% else %}{% url 'admin:plan-unpublish' %}{% endif %}?pks={{event.pk}}';">
+          <label class="custom-control-label" for="planPublishedSwitch">{% trans "Plan published?" %}</label>
+        </div>
+
         <div class="row">
             <div class="col-md-8">
                 <h3 class="block-header">{% trans "Categories" %}</h3>
diff --git a/AKModel/views.py b/AKModel/views.py
index ea9435af..9bb8edc0 100644
--- a/AKModel/views.py
+++ b/AKModel/views.py
@@ -4,6 +4,7 @@ from abc import ABC, abstractmethod
 from itertools import zip_longest
 
 from django.contrib import admin, messages
+from django.db.models.functions import Now
 from django.shortcuts import get_object_or_404, redirect
 from django.urls import reverse_lazy, reverse
 from django.utils.translation import gettext_lazy as _
@@ -474,3 +475,23 @@ class AKResetInterestCounterView(IntermediateAdminActionView):
 
     def action(self, form):
         self.entities.update(interest_counter=0)
+
+
+class PlanPublishView(IntermediateAdminActionView):
+    title = _('Publish plan')
+    model = Event
+    confirmation_message = _('Publish the plan(s) of:')
+    success_message = _('Plan published')
+
+    def action(self, form):
+        self.entities.update(plan_published_at=Now(), plan_hidden=False)
+
+
+class PlanUnpublishView(IntermediateAdminActionView):
+    title = _('Unpublish plan')
+    model = Event
+    confirmation_message = _('Unpublish the plan(s) of:')
+    success_message = _('Plan unpublished')
+
+    def action(self, form):
+        self.entities.update(plan_published_at=None, plan_hidden=True)
-- 
GitLab