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