diff --git a/AKModel/admin.py b/AKModel/admin.py index ad8b76363b9dd4c6c38f6f832f0c1d23fcfcd730..0d48bc6f269a3a886f78da948f7c4d29df8e63c8 100644 --- a/AKModel/admin.py +++ b/AKModel/admin.py @@ -10,7 +10,6 @@ from django.utils import timezone from django.utils.html import format_html from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ -from rest_framework.reverse import reverse from simple_history.admin import SimpleHistoryAdmin from AKModel.availability.models import Availability @@ -18,8 +17,10 @@ from AKModel.forms import RoomFormWithAvailabilities from AKModel.models import Event, AKOwner, AKCategory, AKTrack, AKRequirement, AK, AKSlot, Room, AKOrgaMessage, \ ConstraintViolation, DefaultSlot from AKModel.urls import get_admin_urls_event_wizard, get_admin_urls_event -from AKModel.views import CVMarkResolvedView, CVSetLevelViolationView, CVSetLevelWarningView, AKResetInterestView, \ - AKResetInterestCounterView, PlanPublishView, PlanUnpublishView, DefaultSlotEditorView, RoomBatchCreationView +from AKModel.views.ak import AKResetInterestView, AKResetInterestCounterView +from AKModel.views.manage import PlanPublishView, PlanUnpublishView, DefaultSlotEditorView, CVMarkResolvedView, \ + CVSetLevelViolationView, CVSetLevelWarningView +from AKModel.views.room import RoomBatchCreationView class EventRelatedFieldListFilter(RelatedFieldListFilter): @@ -64,7 +65,7 @@ class EventAdmin(admin.ModelAdmin): @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")) + url=reverse_lazy('admin:event_status', kwargs={'event_slug': obj.slug}), text=_("Status")) @display(description=_("Toggle plan visibility")) def toggle_plan_visibility(self, obj): diff --git a/AKModel/locale/de_DE/LC_MESSAGES/django.po b/AKModel/locale/de_DE/LC_MESSAGES/django.po index 2c101f90249bcf4daa1b5fec826f38d6b2bcc87d..ae516495f48adae3b2f50321701c93fb4af349d4 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: 2023-03-24 17:57+0100\n" +"POT-Creation-Date: 2023-03-26 19:54+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,69 +11,69 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: AKModel/admin.py:64 AKModel/admin.py:67 +#: AKModel/admin.py:65 AKModel/admin.py:68 #: 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 #: AKModel/templates/admin/AKModel/requirements_overview.html:8 -#: AKModel/templates/admin/AKModel/status.html:7 +#: AKModel/templates/admin/AKModel/status/status.html:8 #: AKModel/templates/admin/ak_index.html:15 msgid "Status" msgstr "Status" -#: AKModel/admin.py:69 +#: AKModel/admin.py:70 msgid "Toggle plan visibility" msgstr "Plansichtbarkeit ändern" -#: AKModel/admin.py:73 AKModel/admin.py:84 AKModel/views.py:497 +#: AKModel/admin.py:74 AKModel/admin.py:85 AKModel/views/manage.py:105 msgid "Publish plan" msgstr "Plan veröffentlichen" -#: AKModel/admin.py:76 AKModel/admin.py:89 AKModel/views.py:507 +#: AKModel/admin.py:77 AKModel/admin.py:90 AKModel/views/manage.py:115 msgid "Unpublish plan" msgstr "Plan verbergen" -#: AKModel/admin.py:152 +#: AKModel/admin.py:153 msgid "Wish" msgstr "AK-Wunsch" -#: AKModel/admin.py:158 +#: AKModel/admin.py:159 msgid "Is wish" msgstr "Ist ein Wunsch" -#: AKModel/admin.py:159 +#: AKModel/admin.py:160 msgid "Is not a wish" msgstr "Ist kein Wunsch" -#: AKModel/admin.py:203 +#: AKModel/admin.py:204 msgid "Export to wiki syntax" msgstr "In Wiki-Syntax exportieren" -#: AKModel/admin.py:212 +#: AKModel/admin.py:213 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:227 AKModel/views.py:477 +#: AKModel/admin.py:228 AKModel/views/ak.py:80 msgid "Reset interest in AKs" msgstr "Interesse an AKs zurücksetzen" -#: AKModel/admin.py:232 AKModel/views.py:487 +#: AKModel/admin.py:233 AKModel/views/ak.py:90 msgid "Reset AKs' interest counters" msgstr "Interessenszähler der AKs zurücksetzen" -#: AKModel/admin.py:308 AKModel/admin.py:315 +#: AKModel/admin.py:309 AKModel/admin.py:316 msgid "AK Details" msgstr "AK-Details" -#: AKModel/admin.py:367 AKModel/views.py:447 +#: AKModel/admin.py:368 AKModel/views/manage.py:75 msgid "Mark Constraint Violations as manually resolved" msgstr "Markiere Constraintverletzungen als manuell behoben" -#: AKModel/admin.py:372 AKModel/views.py:457 +#: AKModel/admin.py:373 AKModel/views/manage.py:85 msgid "Set Constraint Violations to level \"violation\"" msgstr "Constraintverletzungen auf Level \"Violation\" setzen" -#: AKModel/admin.py:377 AKModel/views.py:467 +#: AKModel/admin.py:378 AKModel/views/manage.py:95 msgid "Set Constraint Violations to level \"warning\"" msgstr "Constraintverletzungen auf Level \"Warning\" setzen" @@ -102,17 +102,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:128 -#: AKModel/models.py:183 AKModel/models.py:202 AKModel/models.py:223 -#: AKModel/models.py:276 AKModel/models.py:341 AKModel/models.py:374 -#: AKModel/models.py:445 AKModel/models.py:486 AKModel/models.py:651 +#: AKModel/availability/models.py:38 AKModel/models.py:57 AKModel/models.py:129 +#: AKModel/models.py:184 AKModel/models.py:203 AKModel/models.py:224 +#: AKModel/models.py:277 AKModel/models.py:354 AKModel/models.py:387 +#: AKModel/models.py:458 AKModel/models.py:499 AKModel/models.py:664 msgid "Event" msgstr "Event" -#: AKModel/availability/models.py:39 AKModel/models.py:129 -#: AKModel/models.py:184 AKModel/models.py:203 AKModel/models.py:224 -#: AKModel/models.py:277 AKModel/models.py:342 AKModel/models.py:375 -#: AKModel/models.py:446 AKModel/models.py:487 AKModel/models.py:652 +#: AKModel/availability/models.py:39 AKModel/models.py:130 +#: AKModel/models.py:185 AKModel/models.py:204 AKModel/models.py:225 +#: AKModel/models.py:278 AKModel/models.py:355 AKModel/models.py:388 +#: AKModel/models.py:459 AKModel/models.py:500 AKModel/models.py:665 msgid "Associated event" msgstr "Zugehöriges Event" @@ -124,8 +124,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:345 -#: AKModel/models.py:364 AKModel/models.py:495 +#: AKModel/availability/models.py:56 AKModel/models.py:358 +#: AKModel/models.py:377 AKModel/models.py:508 msgid "Room" msgstr "Raum" @@ -133,8 +133,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:285 -#: AKModel/models.py:363 AKModel/models.py:440 +#: AKModel/availability/models.py:65 AKModel/models.py:286 +#: AKModel/models.py:376 AKModel/models.py:453 msgid "AK" msgstr "AK" @@ -142,8 +142,8 @@ msgstr "AK" msgid "AK whose availability this is" msgstr "Verfügbarkeiten" -#: AKModel/availability/models.py:74 AKModel/models.py:187 -#: AKModel/models.py:501 +#: AKModel/availability/models.py:74 AKModel/models.py:188 +#: AKModel/models.py:514 msgid "AK Category" msgstr "AK-Kategorie" @@ -213,7 +213,7 @@ msgstr "" "fürWünsche markieren, z.B. um während der Präsentation auf einem Touchscreen " "ausgefüllt zu werden?" -#: AKModel/forms.py:125 AKModel/models.py:645 +#: AKModel/forms.py:125 AKModel/models.py:658 msgid "Default Slots" msgstr "Standardslots" @@ -244,138 +244,159 @@ msgstr "" msgid "CSV must contain a name column" msgstr "CSV muss eine name-Spalte enthalten" -#: AKModel/models.py:18 AKModel/models.py:175 AKModel/models.py:199 -#: AKModel/models.py:221 AKModel/models.py:239 AKModel/models.py:333 +#: AKModel/metaviews/admin.py:97 AKModel/models.py:28 +msgid "Start" +msgstr "Start" + +#: AKModel/metaviews/admin.py:98 +msgid "Settings" +msgstr "Einstellungen" + +#: AKModel/metaviews/admin.py:99 +msgid "Event created, Prepare Import" +msgstr "Event angelegt, Import vorbereiten" + +#: AKModel/metaviews/admin.py:100 +msgid "Import categories & requirements" +msgstr "Kategorien & Anforderungen kopieren" + +#: AKModel/metaviews/admin.py:101 +msgid "Activate?" +msgstr "Aktivieren?" + +#: AKModel/metaviews/admin.py:102 +#: AKModel/templates/admin/AKModel/event_wizard/activate.html:27 +msgid "Finish" +msgstr "Abschluss" + +#: AKModel/models.py:19 AKModel/models.py:176 AKModel/models.py:200 +#: AKModel/models.py:222 AKModel/models.py:240 AKModel/models.py:346 msgid "Name" msgstr "Name" -#: AKModel/models.py:19 +#: AKModel/models.py:20 msgid "Name or iteration of the event" msgstr "Name oder Iteration des Events" -#: AKModel/models.py:20 +#: AKModel/models.py:21 msgid "Short Form" msgstr "Kurzer Name" -#: AKModel/models.py:21 +#: AKModel/models.py:22 msgid "Short name of letters/numbers/dots/dashes/underscores used in URLs." msgstr "" "Kurzname bestehend aus Buchstaben, Nummern, Punkten und Unterstrichen zur " "Nutzung in URLs" -#: AKModel/models.py:23 +#: AKModel/models.py:24 msgid "Place" msgstr "Ort" -#: AKModel/models.py:24 +#: AKModel/models.py:25 msgid "City etc. the event takes place in" msgstr "Stadt o.ä. in der das Event stattfindet" -#: AKModel/models.py:26 +#: AKModel/models.py:27 msgid "Time Zone" msgstr "Zeitzone" -#: AKModel/models.py:26 +#: AKModel/models.py:27 msgid "Time Zone where this event takes place in" msgstr "Zeitzone in der das Event stattfindet" -#: AKModel/models.py:27 AKModel/views.py:253 -msgid "Start" -msgstr "Start" - -#: AKModel/models.py:27 +#: AKModel/models.py:28 msgid "Time the event begins" msgstr "Zeit zu der das Event beginnt" -#: AKModel/models.py:28 +#: AKModel/models.py:29 msgid "End" msgstr "Ende" -#: AKModel/models.py:28 +#: AKModel/models.py:29 msgid "Time the event ends" msgstr "Zeit zu der das Event endet" -#: AKModel/models.py:29 +#: AKModel/models.py:30 msgid "Resolution Deadline" msgstr "Resolutionsdeadline" -#: AKModel/models.py:30 +#: AKModel/models.py:31 msgid "When should AKs with intention to submit a resolution be done?" msgstr "Wann sollen AKs mit Resolutionsabsicht stattgefunden haben?" -#: AKModel/models.py:32 +#: AKModel/models.py:33 msgid "Interest Window Start" msgstr "Beginn Interessensbekundung" -#: AKModel/models.py:33 +#: AKModel/models.py:34 msgid "Opening time for expression of interest." msgstr "Öffnungszeitpunkt für die Angabe von Interesse an AKs." -#: AKModel/models.py:34 +#: AKModel/models.py:35 msgid "Interest Window End" msgstr "Ende Interessensbekundung" -#: AKModel/models.py:35 +#: AKModel/models.py:36 msgid "Closing time for expression of interest." msgstr "Öffnungszeitpunkt für die Angabe von Interesse an AKs." -#: AKModel/models.py:37 +#: AKModel/models.py:38 msgid "Public event" msgstr "Öffentliches Event" -#: AKModel/models.py:38 +#: AKModel/models.py:39 msgid "Show this event on overview page." msgstr "Zeige dieses Event auf der Ãœbersichtseite an" -#: AKModel/models.py:40 +#: AKModel/models.py:41 msgid "Active State" msgstr "Aktiver Status" -#: AKModel/models.py:40 +#: AKModel/models.py:41 msgid "Marks currently active events" msgstr "Markiert aktuell aktive Events" -#: AKModel/models.py:41 +#: AKModel/models.py:42 msgid "Plan Hidden" msgstr "Plan verborgen" -#: AKModel/models.py:41 +#: AKModel/models.py:42 msgid "Hides plan for non-staff users" msgstr "Verbirgt den Plan für Nutzer*innen ohne erweiterte Rechte" -#: AKModel/models.py:43 +#: AKModel/models.py:44 msgid "Plan published at" msgstr "Plan veröffentlicht am/um" -#: AKModel/models.py:44 +#: AKModel/models.py:45 msgid "Timestamp at which the plan was published" msgstr "Zeitpunkt, zu dem der Plan veröffentlicht wurde" -#: AKModel/models.py:46 +#: AKModel/models.py:47 msgid "Base URL" msgstr "URL-Prefix" -#: AKModel/models.py:46 +#: AKModel/models.py:47 msgid "Prefix for wiki link construction" msgstr "Prefix für die automatische Generierung von Wiki-Links" -#: AKModel/models.py:47 +#: AKModel/models.py:48 msgid "Wiki Export Template Name" msgstr "Wiki-Export Templatename" -#: AKModel/models.py:48 +#: AKModel/models.py:49 msgid "Default Slot Length" msgstr "Standardslotlänge" -#: AKModel/models.py:49 +#: AKModel/models.py:50 msgid "Default length in hours that is assumed for AKs in this event." msgstr "Standardlänge von Slots (in Stunden) für dieses Event" -#: AKModel/models.py:51 +#: AKModel/models.py:52 msgid "Contact email address" msgstr "E-Mail Kontaktadresse" -#: AKModel/models.py:53 +#: AKModel/models.py:54 msgid "" "An email address that is displayed on every page and can be used for all " "kinds of questions" @@ -383,75 +404,75 @@ msgstr "" "Eine Mailadresse die auf jeder Seite angezeigt wird und für alle Arten von " "Fragen genutzt werden kann" -#: AKModel/models.py:57 +#: AKModel/models.py:58 msgid "Events" msgstr "Events" -#: AKModel/models.py:123 +#: AKModel/models.py:124 msgid "Nickname" msgstr "Spitzname" -#: AKModel/models.py:123 +#: AKModel/models.py:124 msgid "Name to identify an AK owner by" msgstr "Name, durch den eine AK-Leitung identifiziert wird" -#: AKModel/models.py:124 +#: AKModel/models.py:125 msgid "Slug" msgstr "Slug" -#: AKModel/models.py:124 +#: AKModel/models.py:125 msgid "Slug for URL generation" msgstr "Slug für URL-Generierung" -#: AKModel/models.py:125 +#: AKModel/models.py:126 msgid "Institution" msgstr "Instutution" -#: AKModel/models.py:125 +#: AKModel/models.py:126 msgid "Uni etc." msgstr "Universität o.ä." -#: AKModel/models.py:126 AKModel/models.py:248 +#: AKModel/models.py:127 AKModel/models.py:249 msgid "Web Link" msgstr "Internet Link" -#: AKModel/models.py:126 +#: AKModel/models.py:127 msgid "Link to Homepage" msgstr "Link zu Homepage oder Webseite" -#: AKModel/models.py:132 AKModel/models.py:494 +#: AKModel/models.py:133 AKModel/models.py:507 msgid "AK Owner" msgstr "AK-Leitung" -#: AKModel/models.py:133 +#: AKModel/models.py:134 msgid "AK Owners" msgstr "AK-Leitungen" -#: AKModel/models.py:175 +#: AKModel/models.py:176 msgid "Name of the AK Category" msgstr "Name der AK-Kategorie" -#: AKModel/models.py:176 AKModel/models.py:200 +#: AKModel/models.py:177 AKModel/models.py:201 msgid "Color" msgstr "Farbe" -#: AKModel/models.py:176 AKModel/models.py:200 +#: AKModel/models.py:177 AKModel/models.py:201 msgid "Color for displaying" msgstr "Farbe für die Anzeige" -#: AKModel/models.py:177 AKModel/models.py:242 +#: AKModel/models.py:178 AKModel/models.py:243 msgid "Description" msgstr "Beschreibung" -#: AKModel/models.py:178 +#: AKModel/models.py:179 msgid "Short description of this AK Category" msgstr "Beschreibung der AK-Kategorie" -#: AKModel/models.py:179 +#: AKModel/models.py:180 msgid "Present by default" msgstr "Defaultmäßig präsentieren" -#: AKModel/models.py:181 +#: AKModel/models.py:182 msgid "" "Present AKs of this category by default if AK owner did not specify whether " "this AK should be presented?" @@ -459,132 +480,132 @@ msgstr "" "AKs dieser Kategorie standardmäßig vorstellen, wenn die Leitungen das für " "ihren AK nicht explizit spezifiziert haben?" -#: AKModel/models.py:188 +#: AKModel/models.py:189 msgid "AK Categories" msgstr "AK-Kategorien" -#: AKModel/models.py:199 +#: AKModel/models.py:200 msgid "Name of the AK Track" msgstr "Name des AK-Tracks" -#: AKModel/models.py:206 +#: AKModel/models.py:207 msgid "AK Track" msgstr "AK-Track" -#: AKModel/models.py:207 +#: AKModel/models.py:208 msgid "AK Tracks" msgstr "AK-Tracks" -#: AKModel/models.py:221 +#: AKModel/models.py:222 msgid "Name of the Requirement" msgstr "Name der Anforderung" -#: AKModel/models.py:227 AKModel/models.py:498 +#: AKModel/models.py:228 AKModel/models.py:511 msgid "AK Requirement" msgstr "AK-Anforderung" -#: AKModel/models.py:228 +#: AKModel/models.py:229 msgid "AK Requirements" msgstr "AK-Anforderungen" -#: AKModel/models.py:239 +#: AKModel/models.py:240 msgid "Name of the AK" msgstr "Name des AKs" -#: AKModel/models.py:240 +#: AKModel/models.py:241 msgid "Short Name" msgstr "Kurzer Name" -#: AKModel/models.py:241 +#: AKModel/models.py:242 msgid "Name displayed in the schedule" msgstr "Name zur Anzeige im AK-Plan" -#: AKModel/models.py:242 +#: AKModel/models.py:243 msgid "Description of the AK" msgstr "Beschreibung des AKs" -#: AKModel/models.py:244 +#: AKModel/models.py:245 msgid "Owners" msgstr "Leitungen" -#: AKModel/models.py:245 +#: AKModel/models.py:246 msgid "Those organizing the AK" msgstr "Menschen, die den AK organisieren und halten" -#: AKModel/models.py:248 +#: AKModel/models.py:249 msgid "Link to wiki page" msgstr "Link zur Wiki Seite" -#: AKModel/models.py:249 +#: AKModel/models.py:250 msgid "Protocol Link" msgstr "Protokolllink" -#: AKModel/models.py:249 +#: AKModel/models.py:250 msgid "Link to protocol" msgstr "Link zum Protokoll" -#: AKModel/models.py:251 +#: AKModel/models.py:252 msgid "Category" msgstr "Kategorie" -#: AKModel/models.py:252 +#: AKModel/models.py:253 msgid "Category of the AK" msgstr "Kategorie des AKs" -#: AKModel/models.py:253 +#: AKModel/models.py:254 msgid "Track" msgstr "Track" -#: AKModel/models.py:254 +#: AKModel/models.py:255 msgid "Track the AK belongs to" msgstr "Track zu dem der AK gehört" -#: AKModel/models.py:256 +#: AKModel/models.py:257 msgid "Resolution Intention" msgstr "Resolutionsabsicht" -#: AKModel/models.py:257 +#: AKModel/models.py:258 msgid "Intends to submit a resolution" msgstr "Beabsichtigt eine Resolution einzureichen" -#: AKModel/models.py:258 +#: AKModel/models.py:259 msgid "Present this AK" msgstr "AK präsentieren" -#: AKModel/models.py:259 +#: AKModel/models.py:260 msgid "Present results of this AK" msgstr "Die Ergebnisse dieses AKs vorstellen" -#: AKModel/models.py:261 AKModel/templates/admin/AKModel/status.html:105 +#: AKModel/models.py:262 AKModel/views/status.py:138 msgid "Requirements" msgstr "Anforderungen" -#: AKModel/models.py:262 +#: AKModel/models.py:263 msgid "AK's Requirements" msgstr "Anforderungen des AKs" -#: AKModel/models.py:264 +#: AKModel/models.py:265 msgid "Conflicting AKs" msgstr "AK-Konflikte" -#: AKModel/models.py:265 +#: AKModel/models.py:266 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:266 +#: AKModel/models.py:267 msgid "Prerequisite AKs" msgstr "Vorausgesetzte AKs" -#: AKModel/models.py:267 +#: AKModel/models.py:268 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:269 +#: AKModel/models.py:270 msgid "Organizational Notes" msgstr "Notizen zur Organisation" -#: AKModel/models.py:270 +#: AKModel/models.py:271 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/" @@ -594,287 +615,287 @@ msgstr "" "Anmerkungen bitte den Button für Direktnachrichten verwenden (nach dem " "Anlegen/Bearbeiten)." -#: AKModel/models.py:272 +#: AKModel/models.py:273 msgid "Interest" msgstr "Interesse" -#: AKModel/models.py:272 +#: AKModel/models.py:273 msgid "Expected number of people" msgstr "Erwartete Personenzahl" -#: AKModel/models.py:273 +#: AKModel/models.py:274 msgid "Interest Counter" msgstr "Interessenszähler" -#: AKModel/models.py:274 +#: AKModel/models.py:275 msgid "People who have indicated interest online" msgstr "Anzahl Personen, die online Interesse bekundet haben" -#: AKModel/models.py:279 +#: AKModel/models.py:280 msgid "Export?" msgstr "Export?" -#: AKModel/models.py:280 +#: AKModel/models.py:281 msgid "Include AK in wiki export?" msgstr "AK bei Wiki-Export berücksichtigen?" -#: AKModel/models.py:286 AKModel/models.py:489 -#: AKModel/templates/admin/AKModel/status.html:57 -#: AKModel/templates/admin/AKModel/status.html:64 AKModel/views.py:375 +#: AKModel/models.py:287 AKModel/models.py:502 +#: AKModel/templates/admin/AKModel/status/event_aks.html:10 +#: AKModel/views/manage.py:55 AKModel/views/status.py:76 msgid "AKs" msgstr "AKs" -#: AKModel/models.py:333 +#: AKModel/models.py:346 msgid "Name or number of the room" msgstr "Name oder Nummer des Raums" -#: AKModel/models.py:334 +#: AKModel/models.py:347 msgid "Location" msgstr "Ort" -#: AKModel/models.py:335 +#: AKModel/models.py:348 msgid "Name or number of the location" msgstr "Name oder Nummer des Ortes" -#: AKModel/models.py:336 +#: AKModel/models.py:349 msgid "Capacity" msgstr "Kapazität" -#: AKModel/models.py:337 +#: AKModel/models.py:350 msgid "Maximum number of people (-1 for unlimited)." msgstr "Maximale Personenzahl (-1 wenn unbeschränkt)." -#: AKModel/models.py:338 +#: AKModel/models.py:351 msgid "Properties" msgstr "Eigenschaften" -#: AKModel/models.py:339 +#: AKModel/models.py:352 msgid "AK requirements fulfilled by the room" msgstr "AK-Anforderungen, die dieser Raum erfüllt" -#: AKModel/models.py:346 AKModel/templates/admin/AKModel/status.html:40 +#: AKModel/models.py:359 AKModel/views/status.py:46 msgid "Rooms" msgstr "Räume" -#: AKModel/models.py:363 +#: AKModel/models.py:376 msgid "AK being mapped" msgstr "AK, der zugeordnet wird" -#: AKModel/models.py:365 +#: AKModel/models.py:378 msgid "Room the AK will take place in" msgstr "Raum in dem der AK stattfindet" -#: AKModel/models.py:366 AKModel/models.py:648 +#: AKModel/models.py:379 AKModel/models.py:661 msgid "Slot Begin" msgstr "Beginn des Slots" -#: AKModel/models.py:366 AKModel/models.py:648 +#: AKModel/models.py:379 AKModel/models.py:661 msgid "Time and date the slot begins" msgstr "Zeit und Datum zu der der AK beginnt" -#: AKModel/models.py:368 +#: AKModel/models.py:381 msgid "Duration" msgstr "Dauer" -#: AKModel/models.py:369 +#: AKModel/models.py:382 msgid "Length in hours" msgstr "Länge in Stunden" -#: AKModel/models.py:371 +#: AKModel/models.py:384 msgid "Scheduling fixed" msgstr "Planung fix" -#: AKModel/models.py:372 +#: 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:377 +#: AKModel/models.py:390 msgid "Last update" msgstr "Letzte Aktualisierung" -#: AKModel/models.py:380 +#: AKModel/models.py:393 msgid "AK Slot" msgstr "AK-Slot" -#: AKModel/models.py:381 AKModel/models.py:491 +#: AKModel/models.py:394 AKModel/models.py:504 msgid "AK Slots" msgstr "AK-Slot" -#: AKModel/models.py:403 AKModel/models.py:412 +#: AKModel/models.py:416 AKModel/models.py:425 msgid "Not scheduled yet" msgstr "Noch nicht geplant" -#: AKModel/models.py:441 +#: AKModel/models.py:454 msgid "AK this message belongs to" msgstr "AK zu dem die Nachricht gehört" -#: AKModel/models.py:442 +#: AKModel/models.py:455 msgid "Message text" msgstr "Nachrichtentext" -#: AKModel/models.py:443 +#: 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:449 +#: AKModel/models.py:462 msgid "AK Orga Message" msgstr "AK-Organachricht" -#: AKModel/models.py:450 +#: AKModel/models.py:463 msgid "AK Orga Messages" msgstr "AK-Organachrichten" -#: AKModel/models.py:459 +#: AKModel/models.py:472 msgid "Constraint Violation" msgstr "Constraintverletzung" -#: AKModel/models.py:460 AKModel/templates/admin/AKModel/status.html:87 +#: AKModel/models.py:473 AKModel/views/status.py:95 msgid "Constraint Violations" msgstr "Constraintverletzungen" -#: AKModel/models.py:464 +#: AKModel/models.py:477 msgid "Owner has two parallel slots" msgstr "Leitung hat zwei Slots parallel" -#: AKModel/models.py:465 +#: 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:466 +#: 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:467 +#: 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:468 +#: 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:469 +#: 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:471 +#: 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:472 +#: 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:473 +#: 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:474 +#: 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:475 +#: 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:478 +#: AKModel/models.py:491 msgid "Warning" msgstr "Warnung" -#: AKModel/models.py:479 +#: AKModel/models.py:492 msgid "Violation" msgstr "Verletzung" -#: AKModel/models.py:481 +#: AKModel/models.py:494 msgid "Type" msgstr "Art" -#: AKModel/models.py:482 +#: 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:483 +#: AKModel/models.py:496 msgid "Level" msgstr "Level" -#: AKModel/models.py:484 +#: AKModel/models.py:497 msgid "Severity level of the violation" msgstr "Schweregrad der Verletzung" -#: AKModel/models.py:490 +#: AKModel/models.py:503 msgid "AK(s) belonging to this constraint" msgstr "AK(s), die zu diesem Constraint gehören" -#: AKModel/models.py:492 +#: 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:494 +#: AKModel/models.py:507 msgid "AK Owner belonging to this constraint" msgstr "AK Leitung(en), die zu diesem Constraint gehören" -#: AKModel/models.py:496 +#: AKModel/models.py:509 msgid "Room belonging to this constraint" msgstr "Raum, der zu diesem Constraint gehört" -#: AKModel/models.py:499 +#: AKModel/models.py:512 msgid "AK Requirement belonging to this constraint" msgstr "AK Anforderung, die zu diesem Constraint gehört" -#: AKModel/models.py:501 +#: AKModel/models.py:514 msgid "AK Category belonging to this constraint" msgstr "AK Kategorie, di zu diesem Constraint gehört" -#: AKModel/models.py:503 +#: AKModel/models.py:516 msgid "Comment" msgstr "Kommentar" -#: AKModel/models.py:503 +#: AKModel/models.py:516 msgid "Comment or further details for this violation" msgstr "Kommentar oder weitere Details zu dieser Vereletzung" -#: AKModel/models.py:506 +#: AKModel/models.py:519 msgid "Timestamp" msgstr "Timestamp" -#: AKModel/models.py:506 +#: AKModel/models.py:519 msgid "Time of creation" msgstr "Zeitpunkt der ERstellung" -#: AKModel/models.py:507 +#: AKModel/models.py:520 msgid "Manually Resolved" msgstr "Manuell behoben" -#: AKModel/models.py:508 +#: AKModel/models.py:521 msgid "Mark this violation manually as resolved" msgstr "Markiere diese Verletzung manuell als behoben" -#: AKModel/models.py:535 +#: AKModel/models.py:548 #: AKModel/templates/admin/AKModel/requirements_overview.html:27 msgid "Details" msgstr "Details" -#: AKModel/models.py:644 +#: AKModel/models.py:657 msgid "Default Slot" msgstr "Standardslot" -#: AKModel/models.py:649 +#: AKModel/models.py:662 msgid "Slot End" msgstr "Ende des Slots" -#: AKModel/models.py:649 +#: AKModel/models.py:662 msgid "Time and date the slot ends" msgstr "Zeit und Datum zu der der Slot endet" -#: AKModel/models.py:654 +#: AKModel/models.py:667 msgid "Primary categories" msgstr "Primäre Kategorien" -#: AKModel/models.py:655 +#: AKModel/models.py:668 msgid "Categories that should be assigned to this slot primarily" msgstr "Kategorieren, die diesem Slot primär zugewiesen werden sollen" @@ -925,11 +946,6 @@ msgstr "Assistent zum Anlegen eines neuen Events" 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:258 -msgid "Finish" -msgstr "Abschluss" - #: AKModel/templates/admin/AKModel/event_wizard/created_prepare_import.html:16 msgid "New event:" msgstr "Neues Event:" @@ -991,7 +1007,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:121 +#: AKModel/views/status.py:152 msgid "Add Requirement" msgstr "Anforderung hinzufügen" @@ -1012,99 +1028,42 @@ msgstr "Sichern und weiter bearbeiten" msgid "Save" msgstr "Sichern" -#: 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:25 -msgid "No categories yet" -msgstr "Bisher keine Kategorien" - -#: AKModel/templates/admin/AKModel/status.html:38 -msgid "Add category" -msgstr "Kategorie hinzufügen" - -#: AKModel/templates/admin/AKModel/status.html:42 -msgid "No rooms yet" -msgstr "Bisher keine Räume" - -#: AKModel/templates/admin/AKModel/status.html:54 -msgid "Add Room" -msgstr "Raum hinzufügen" - -#: AKModel/templates/admin/AKModel/status.html:55 AKModel/views.py:613 -msgid "Import Rooms from CSV" -msgstr "Räume aus CSV importieren" - -#: AKModel/templates/admin/AKModel/status.html:59 +#: AKModel/templates/admin/AKModel/status/event_aks.html:5 msgid "No AKs yet" msgstr "Bisher keine AKs" -#: AKModel/templates/admin/AKModel/status.html:67 +#: AKModel/templates/admin/AKModel/status/event_aks.html:13 msgid "Slots" msgstr "Slots" -#: AKModel/templates/admin/AKModel/status.html:70 +#: AKModel/templates/admin/AKModel/status/event_aks.html:16 msgid "Unscheduled Slots" msgstr "Ungeplante Slots" -#: AKModel/templates/admin/AKModel/status.html:84 -#: AKModel/templates/admin/ak_index.html:16 -msgid "Scheduling" -msgstr "Scheduling" - -#: AKModel/templates/admin/AKModel/status.html:89 -msgid "AKs requiring special attention" -msgstr "AKs, die besondere Aufmerksamkeit benötigen" - -#: AKModel/templates/admin/AKModel/status.html:91 -msgid "Enter Interest" -msgstr "Interesse erfassen" - -#: AKModel/templates/admin/AKModel/status.html:94 AKModel/views.py:519 -msgid "Edit Default Slots" -msgstr "Standardslots bearbeiten" - -#: AKModel/templates/admin/AKModel/status.html:96 -msgid "Manage ak tracks" -msgstr "AK-Tracks verwalten" - -#: AKModel/templates/admin/AKModel/status.html:98 -msgid "Export AKs as CSV" -msgstr "AKs als CSV exportieren" - -#: AKModel/templates/admin/AKModel/status.html:100 -msgid "Export AKs for Wiki" -msgstr "AKs im Wiki-Format exportieren" +#: AKModel/templates/admin/AKModel/status/event_categories.html:4 +msgid "No categories yet" +msgstr "Bisher keine Kategorien" -#: AKModel/templates/admin/AKModel/status.html:102 AKModel/views.py:345 -msgid "Export AK Slides" -msgstr "AK-Folien exportieren" +#: AKModel/templates/admin/AKModel/status/event_overview.html:12 +msgid "Plan published?" +msgstr "Plan veröffentlicht?" -#: AKModel/templates/admin/AKModel/status.html:107 +#: AKModel/templates/admin/AKModel/status/event_requirements.html:4 msgid "No requirements yet" msgstr "Bisher keine Anforderungen" -#: AKModel/templates/admin/AKModel/status.html:120 -msgid "Show AKs for requirements" -msgstr "Zu Anforderungen gehörige AKs anzeigen" - -#: AKModel/templates/admin/AKModel/status.html:124 -msgid "Messages" -msgstr "Nachrichten" - -#: AKModel/templates/admin/AKModel/status.html:126 -msgid "Delete all messages" -msgstr "Alle Nachrichten löschen" +#: AKModel/templates/admin/AKModel/status/event_rooms.html:4 +msgid "No rooms yet" +msgstr "Bisher keine Räume" #: AKModel/templates/admin/ak_index.html:7 msgid "Active Events" msgstr "Aktive Events" +#: AKModel/templates/admin/ak_index.html:16 AKModel/views/status.py:87 +msgid "Scheduling" +msgstr "Scheduling" + #: AKModel/templates/admin/login.html:8 msgid "Please correct the error below." msgstr "Bitte den untenstehenden Fehler korrigieren." @@ -1134,167 +1093,209 @@ msgstr "Login" msgid "Register" msgstr "Registrieren" -#: AKModel/views.py:156 -msgid "Event Status" -msgstr "Eventstatus" - -#: AKModel/views.py:169 +#: AKModel/views/ak.py:14 msgid "Requirements for Event" msgstr "Anforderungen für das Event" -#: AKModel/views.py:183 +#: AKModel/views/ak.py:28 msgid "AK CSV Export" msgstr "AK-CSV-Export" -#: AKModel/views.py:197 +#: AKModel/views/ak.py:42 msgid "AK Wiki Export" msgstr "AK-Wiki-Export" -#: AKModel/views.py:208 AKModel/views.py:361 +#: AKModel/views/ak.py:53 AKModel/views/manage.py:41 msgid "Wishes" msgstr "Wünsche" -#: AKModel/views.py:229 +#: AKModel/views/ak.py:60 msgid "Delete AK Orga Messages" msgstr "AK-Organachrichten löschen" -#: AKModel/views.py:244 +#: AKModel/views/ak.py:75 msgid "AK Orga Messages successfully deleted" msgstr "AK-Organachrichten erfolgreich gelöscht" -#: AKModel/views.py:254 -msgid "Settings" -msgstr "Einstellungen" +#: AKModel/views/ak.py:82 +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:255 -msgid "Event created, Prepare Import" -msgstr "Event angelegt, Import vorbereiten" +#: AKModel/views/ak.py:83 +msgid "Reset of interest in AKs successful." +msgstr "Interesse an AKs erfolgreich zurückgesetzt." -#: AKModel/views.py:256 -msgid "Import categories & requirements" -msgstr "Kategorien & Anforderungen kopieren" +#: AKModel/views/ak.py:92 +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:257 -msgid "Activate?" -msgstr "Aktivieren?" +#: AKModel/views/ak.py:93 +msgid "AKs' interest counters set back to 0." +msgstr "Interessenszähler der AKs zurückgesetzt" -#: AKModel/views.py:320 +#: AKModel/views/event_wizard.py:69 #, python-format msgid "Copied '%(obj)s'" msgstr "'%(obj)s' kopiert" -#: AKModel/views.py:323 +#: AKModel/views/event_wizard.py:72 #, python-format msgid "Could not copy '%(obj)s' (%(error)s)" msgstr "'%(obj)s' konnte nicht kopiert werden (%(error)s)" -#: AKModel/views.py:356 +#: AKModel/views/manage.py:25 AKModel/views/status.py:127 +msgid "Export AK Slides" +msgstr "AK-Folien exportieren" + +#: AKModel/views/manage.py:36 msgid "Symbols" msgstr "Symbole" -#: AKModel/views.py:357 +#: AKModel/views/manage.py:37 msgid "Who?" msgstr "Wer?" -#: AKModel/views.py:358 +#: AKModel/views/manage.py:38 msgid "Duration(s)" msgstr "Dauer(n)" -#: AKModel/views.py:359 +#: AKModel/views/manage.py:39 msgid "Reso intention?" msgstr "Resolutionsabsicht?" -#: AKModel/views.py:360 +#: AKModel/views/manage.py:40 msgid "Category (for Wishes)" msgstr "Kategorie (für Wünsche)" -#: AKModel/views.py:449 +#: AKModel/views/manage.py:77 msgid "The following Constraint Violations will be marked as manually resolved" msgstr "" "Die folgenden Constraintverletzungen werden als manuell behoben markiert." -#: AKModel/views.py:450 +#: AKModel/views/manage.py:78 msgid "Constraint Violations marked as resolved" msgstr "Constraintverletzungen als manuell behoben markiert" -#: AKModel/views.py:459 +#: AKModel/views/manage.py:87 msgid "The following Constraint Violations will be set to level 'violation'" msgstr "" "Die folgenden Constraintverletzungen werden auf das Level \"Violation\" " "gesetzt." -#: AKModel/views.py:460 +#: AKModel/views/manage.py:88 msgid "Constraint Violations set to level 'violation'" msgstr "Constraintverletzungen auf Level \"Violation\" gesetzt" -#: AKModel/views.py:469 +#: AKModel/views/manage.py:97 msgid "The following Constraint Violations will be set to level 'warning'" msgstr "" "Die folgenden Constraintverletzungen werden auf das Level 'warning' gesetzt." -#: AKModel/views.py:470 +#: AKModel/views/manage.py:98 msgid "Constraint Violations set to level 'warning'" msgstr "Constraintverletzungen auf Level \"Warning\" gesetzt" -#: AKModel/views.py:479 -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:480 -msgid "Reset of interest in AKs successful." -msgstr "Interesse an AKs erfolgreich zurückgesetzt." - -#: AKModel/views.py:489 -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:490 -msgid "AKs' interest counters set back to 0." -msgstr "Interessenszähler der AKs zurückgesetzt" - -#: AKModel/views.py:499 +#: AKModel/views/manage.py:107 msgid "Publish the plan(s) of:" msgstr "Den Plan/die Pläne veröffentlichen von:" -#: AKModel/views.py:500 +#: AKModel/views/manage.py:108 msgid "Plan published" msgstr "Plan veröffentlicht" -#: AKModel/views.py:509 +#: AKModel/views/manage.py:117 msgid "Unpublish the plan(s) of:" msgstr "Den Plan/die Pläne verbergen von:" -#: AKModel/views.py:510 +#: AKModel/views/manage.py:118 msgid "Plan unpublished" msgstr "Plan verborgen" -#: AKModel/views.py:556 +#: AKModel/views/manage.py:127 AKModel/views/status.py:111 +msgid "Edit Default Slots" +msgstr "Standardslots bearbeiten" + +#: AKModel/views/manage.py:164 #, python-brace-format msgid "Could not update slot {id} since it does not belong to {event}" msgstr "" "Konnte Slot {id} nicht bearbeiten, da er nicht zum Event {event} gehört" -#: AKModel/views.py:586 +#: AKModel/views/manage.py:194 #, python-brace-format msgid "Updated {u} slot(s). created {c} new slot(s) and deleted {d} slot(s)" msgstr "" "{u} Slot(s) aktualisiert, {c} Slot(s) hinzugefügt und {d} Slot(s) gelöscht" -#: AKModel/views.py:607 +#: AKModel/views/room.py:31 #, python-format msgid "Created Room '%(room)s'" msgstr "Raum '%(room)s angelegt" -#: AKModel/views.py:644 +#: AKModel/views/room.py:37 AKModel/views/status.py:66 +msgid "Import Rooms from CSV" +msgstr "Räume aus CSV importieren" + +#: AKModel/views/room.py:68 #, python-brace-format msgid "Could not import room {name}: {e}" msgstr "Konnte Raum {name} nicht importieren: {e}" -#: AKModel/views.py:648 +#: AKModel/views/room.py:72 #, python-brace-format msgid "Imported {count} room(s)" msgstr "{count} Raum/Räume importiert" -#: AKModel/views.py:650 +#: AKModel/views/room.py:74 msgid "No rooms imported" msgstr "Keine Räume importiert" + +#: AKModel/views/status.py:16 +msgid "Overview" +msgstr "Ãœberblick" + +#: AKModel/views/status.py:26 +msgid "Categories" +msgstr "Kategorien" + +#: AKModel/views/status.py:30 +msgid "Add category" +msgstr "Kategorie hinzufügen" + +#: AKModel/views/status.py:50 +msgid "Add Room" +msgstr "Raum hinzufügen" + +#: AKModel/views/status.py:100 +msgid "AKs requiring special attention" +msgstr "AKs, die besondere Aufmerksamkeit benötigen" + +#: AKModel/views/status.py:104 +msgid "Enter Interest" +msgstr "Interesse erfassen" + +#: AKModel/views/status.py:115 +msgid "Manage ak tracks" +msgstr "AK-Tracks verwalten" + +#: AKModel/views/status.py:119 +msgid "Export AKs as CSV" +msgstr "AKs als CSV exportieren" + +#: AKModel/views/status.py:123 +msgid "Export AKs for Wiki" +msgstr "AKs im Wiki-Format exportieren" + +#: AKModel/views/status.py:148 +msgid "Show AKs for requirements" +msgstr "Zu Anforderungen gehörige AKs anzeigen" + +#: AKModel/views/status.py:159 +msgid "Event Status" +msgstr "Eventstatus" + +#~ msgid "Messages" +#~ msgstr "Nachrichten" + +#~ msgid "Delete all messages" +#~ msgstr "Alle Nachrichten löschen" diff --git a/AKModel/metaviews/__init__.py b/AKModel/metaviews/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f5a3ee6812b530245b2f052325432c6f85b23207 --- /dev/null +++ b/AKModel/metaviews/__init__.py @@ -0,0 +1,3 @@ +from AKModel.metaviews.status import StatusManager + +status_manager = StatusManager() diff --git a/AKModel/metaviews/admin.py b/AKModel/metaviews/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..9c6f90279875e267c67aae46183d6e50e749d9ce --- /dev/null +++ b/AKModel/metaviews/admin.py @@ -0,0 +1,158 @@ +from abc import ABC, abstractmethod + +from django.contrib import admin, messages +from django.shortcuts import get_object_or_404 +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ +from django.views.generic import FormView + +from AKModel.forms import AdminIntermediateForm, AdminIntermediateActionForm +from AKModel.models import Event + + +class EventSlugMixin: + """ + Mixin to handle views with event slugs + """ + event = None + + def _load_event(self): + # Find event based on event slug + if self.event is None: + self.event = get_object_or_404(Event, slug=self.kwargs.get("event_slug", None)) + + def get(self, request, *args, **kwargs): + self._load_event() + return super().get(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + self._load_event() + return super().post(request, *args, **kwargs) + + def list(self, request, *args, **kwargs): + self._load_event() + return super().list(request, *args, **kwargs) + + def create(self, request, *args, **kwargs): + self._load_event() + return super().create(request, *args, **kwargs) + + def dispatch(self, request, *args, **kwargs): + if self.event is None: + self._load_event() + return super().dispatch(request, *args, **kwargs) + + def get_context_data(self, *, object_list=None, **kwargs): + context = super().get_context_data(object_list=object_list, **kwargs) + # Add event to context (to make it accessible in templates) + context["event"] = self.event + return context + + +class FilterByEventSlugMixin(EventSlugMixin): + """ + Mixin to filter different querysets based on a event slug from the request url + """ + + def get_queryset(self): + # Filter current queryset based on url event slug or return 404 if event slug is invalid + return super().get_queryset().filter(event=self.event) + + +class AdminViewMixin: + site_url = '' + title = '' + + def get_context_data(self, **kwargs): + extra = admin.site.each_context(self.request) + extra.update(super().get_context_data(**kwargs)) + + if self.site_url != '': + extra["site_url"] = self.site_url + if self.title != '': + extra["title"] = self.title + + return extra + + +class IntermediateAdminView(AdminViewMixin, FormView): + template_name = "admin/AKModel/action_intermediate.html" + form_class = AdminIntermediateForm + + def get_preview(self): + return "" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["title"] = self.title + context["preview"] = self.get_preview() + return context + + +class WizardViewMixin: + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["wizard_step"] = self.wizard_step + context["wizard_steps"] = [ + _("Start"), + _("Settings"), + _("Event created, Prepare Import"), + _("Import categories & requirements"), + _("Activate?"), + _("Finish") + ] + context["wizard_step_text"] = context["wizard_steps"][self.wizard_step - 1] + context["wizard_steps_total"] = len(context["wizard_steps"]) + return context + + +class IntermediateAdminActionView(IntermediateAdminView, ABC): + form_class = AdminIntermediateActionForm + entities = None + + 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): + 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 action(self, form): + pass + + def form_valid(self, form): + 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 diff --git a/AKModel/metaviews/status.py b/AKModel/metaviews/status.py new file mode 100644 index 0000000000000000000000000000000000000000..2e1c3777d18f4b1e09f45e9bfad633eca338fe49 --- /dev/null +++ b/AKModel/metaviews/status.py @@ -0,0 +1,150 @@ +from abc import ABC, abstractmethod +from collections import defaultdict + +from django.template import loader +from django.views.generic import TemplateView + +from AKModel.metaviews.admin import AdminViewMixin + + +class StatusWidget(ABC): + title = "Status Widget" + actions = [] + status = "primary" + + @property + @abstractmethod + def required_context_type(self) -> str: + """ + Which model/context is needed to render this widget? + """ + pass + + def get_context_data(self, context) -> dict: + """ + Allow to manipulate the context + :return: context (with or without changes) + """ + return context + + def render(self, context: {}, request) -> dict: + """ + Render widget based on context + + :param context: Context for rendering + :return: Dictionary containing the rendered/prepared information + """ + context = self.get_context_data(context) + return { + "body": self.render_body(context, request), + "title": self.render_title(context), + "actions": self.render_actions(context), + "status": self.render_status(context), + } + + def render_title(self, context: {}) -> str: + """ + Render title for widget based on context + + By default, the title attribute is used without modification + :param context: Context for rendering + :return: Rendered title + """ + return self.title + + def render_status(self, context: {}) -> str: + """ + Render status for widget based on context + + By default, the status attribute is used without modification + :param context: Context for rendering + :return: Rendered title + """ + return self.status + + @abstractmethod + def render_body(self, context: {}, request) -> str: + """ + Render body for widget based on context + + :param context: Context for rendering + :return: Rendered widget body (HTML) + """ + pass + + def render_actions(self, context: {}) -> list[dict]: + """ + Render actions for widget based on context + + By default, all actions specified for this widget are returned without modification + + :param context: Context for rendering + :return: List of actions + """ + return [a for a in self.actions] + + +class TemplateStatusWidget(StatusWidget): + @property + @abstractmethod + def template_name(self) -> str: + pass + + def render_body(self, context: {}, request) -> str: + template = loader.get_template(self.template_name) + return template.render(context, request) + + +class StatusManager: + """ + Registry for all status widgets + """ + widgets = {} + widgets_by_context_type = defaultdict(list) + + def register(self, name: str): + """ + Call this as + @status_manager.register(name="xyz") + to register a status widget + + :param name: name of this widget (only used internally). Has to be unique. + """ + def _register(widget_class): + w = widget_class() + self.widgets[name] = w + self.widgets_by_context_type[w.required_context_type].append(w) + return widget_class + + return _register + + def get_by_context_type(self, context_type: str): + """ + Filter widgets for ones suitable for provided context + + :param context_type: name of the model provided as context + :return: a list of all matching widgets + """ + if context_type in self.widgets_by_context_type.keys(): + return self.widgets_by_context_type[context_type] + return [] + + +class StatusView(ABC, AdminViewMixin, TemplateView): + template_name = "admin/AKModel/status/status.html" + + @property + @abstractmethod + def provided_context_type(self) -> str: + """ + Which model/context is provided by this status view? + """ + pass + + def get(self, request, *args, **kwargs): + context = self.get_context_data(**kwargs) + + from AKModel.metaviews import status_manager + context['widgets'] = [w.render(context, self.request) for w in status_manager.get_by_context_type(self.provided_context_type)] + + return self.render_to_response(context) diff --git a/AKModel/templates/admin/AKModel/render_ak_messages.html b/AKModel/templates/admin/AKModel/render_ak_messages.html index e62ccbb76c2daeba0b9a91b1d26b5953127b9249..fbadbc138817c2bd36df9bff9e67e6ca0522fb35 100644 --- a/AKModel/templates/admin/AKModel/render_ak_messages.html +++ b/AKModel/templates/admin/AKModel/render_ak_messages.html @@ -1,11 +1,15 @@ -<table class="table table-striped"> -{% for message in ak_messages %} - <tr><td> - <span class="text-secondary float-end"> - {{ message.timestamp|date:"Y-m-d H:i:s" }} - </span> - <h5><a href="{{ message.ak.detail_url }}">{{ message.ak }}</a></h5> - <p>{{ message.text }}</p> - </td></tr> -{% endfor %} -</table> +{% load tz %} + +{% timezone event.timezone %} + <table class="table table-striped"> + {% for message in ak_messages %} + <tr><td> + <span class="text-secondary float-end"> + {{ message.timestamp|date:"Y-m-d H:i:s" }} + </span> + <h5><a href="{{ message.ak.detail_url }}">{{ message.ak }}</a></h5> + <p>{{ message.text }}</p> + </td></tr> + {% endfor %} + </table> +{% endtimezone %} diff --git a/AKModel/templates/admin/AKModel/status.html b/AKModel/templates/admin/AKModel/status.html deleted file mode 100644 index 8a53f8cd389d70d2765b73e3db289919647489c0..0000000000000000000000000000000000000000 --- a/AKModel/templates/admin/AKModel/status.html +++ /dev/null @@ -1,130 +0,0 @@ -{% extends "admin/base_site.html" %} -{% load tags_AKModel %} - -{% load i18n %} -{% load tz %} - -{% block title %}{% trans "Status" %}: {{event}}{% endblock %} - -{% block content %} - {% timezone event.timezone %} - <h2><a href="{% url 'admin:AKModel_event_change' event.pk %}">{{event}}</a></h2> - <h5>{{ event.start }} - {{ event.end }}</h5> - - <div class="form-check form-switch mt-2 mb-2"> - <input type="checkbox" class="form-check-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="form-check-label" for="planPublishedSwitch">{% trans "Plan published?" %}</label> - </div> - - <div class="row"> - <div class="col-md-8"> - <h3 class="block-header">{% trans "Categories" %}</h3> - {% if event.akcategory_set.count == 0 %} - <p class="text-danger">{% trans "No categories yet" %}</p> - {% else %} - <p> - {{ event.akcategory_set.count }}: - {% for category in event.akcategory_set.all %} - {% if forloop.counter0 > 0 %} - · - {% endif %} - <a href="{% url 'admin:AKModel_akcategory_change' category.pk %}">{{ category }}</a> - ({{ category.ak_set.count }}) - {% endfor %} - </p> - {% endif %} - <a class="btn btn-success" href="{% url 'admin:AKModel_akcategory_add' %}">{% trans "Add category" %}</a> - - <h3 class="block-header">{% trans "Rooms" %}</h3> - {% if event.room_set.count == 0 %} - <p class="text-danger">{% trans "No rooms yet" %}</p> - {% else %} - <p> - {{ event.room_set.count }}: - {% for room in event.room_set.all %} - {% if forloop.counter0 > 0 %} - · - {% endif %} - <a href="{% url 'admin:AKModel_room_change' room.pk %}">{{ room }}</a> - {% endfor %} - </p> - {% endif %} - <a class="btn btn-success" href="{% url 'admin:AKModel_room_add' %}">{% trans "Add Room" %}</a> - <a class="btn btn-success" href="{% url 'admin:room-import' event_slug=event.slug %}">{% trans "Import Rooms from CSV" %}</a> - - <h3 class="block-header">{% trans "AKs" %}</h3> - {% if event.ak_set.count == 0 %} - <p class="text-danger">{% trans "No AKs yet" %}</p> - {% else %} - <table> - <tbody> - <tr> - <td>{% trans "AKs" %}</td><td>{{ event.ak_set.count }}</td> - </tr> - <tr> - <td>{% trans "Slots" %}</td><td>{{ event.akslot_set.count }}</td> - </tr> - <tr> - <td>{% trans "Unscheduled Slots" %}</td><td> - {% if "AKScheduling"|check_app_installed %} - <a href="{% url 'admin:slots_unscheduled' event_slug=event.slug %}"> - {{ unscheduled_slots_count }} - </a> - {% else %} - {{ unscheduled_slots_count }} - {% endif %} - </td> - </tr> - </tbody> - </table> - - <a class="btn btn-success" - href="{% url 'admin:schedule' event_slug=event.slug %}">{% trans "Scheduling" %}</a> - {% if "AKScheduling | is_installed" %} - <a class="btn btn-success" - href="{% url 'admin:constraint-violations' slug=event.slug %}">{% trans "Constraint Violations" %} <span class="badge bg-secondary">{{ event.constraintviolation_set.count }}</span></a> - <a class="btn btn-success" - href="{% url 'admin:special-attention' slug=event.slug %}">{% trans "AKs requiring special attention" %}</a> - <a class="btn btn-success" - href="{% url 'admin:enter-interest' event_slug=event.slug pk=event.ak_set.all.first.pk %}">{% trans "Enter Interest" %}</a> - {% endif %} - <a class="btn btn-success" - href="{% url 'admin:default-slots-editor' event_slug=event.slug %}">{% trans "Edit Default Slots" %}</a> - <a class="btn btn-success" - href="{% url 'admin:tracks_manage' event_slug=event.slug %}">{% trans "Manage ak tracks" %}</a> - <a class="btn btn-success" - href="{% url 'admin:ak_csv_export' event_slug=event.slug %}">{% trans "Export AKs as CSV" %}</a> - <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 %}">{% trans "Export AK Slides" %}</a> - {% endif %} - - <h3 class="block-header">{% trans "Requirements" %}</h3> - {% if event.akrequirement_set.count == 0 %} - <p class="text-danger">{% trans "No requirements yet" %}</p> - {% else %} - <p> - {{ event.akrequirement_set.count }}: - {% for requirement in event.akrequirement_set.all %} - {% if forloop.counter0 > 0 %} - · - {% endif %} - <a href="{% url 'admin:AKModel_akrequirement_change' requirement.pk %}">{{ requirement }}</a> - ({{ requirement.ak_set.count }}) - {% endfor %} - </p> - {% endif %} - <a class="btn btn-success" href="{% url 'admin:event_requirement_overview' event.slug %}">{% trans "Show AKs for requirements" %}</a> - <a class="btn btn-success" href="{% url 'admin:AKModel_akrequirement_add' %}">{% trans "Add Requirement" %}</a> - </div> - <div class="col-md-4"> - <h3 class="block-header">{% trans "Messages" %}</h3> - {% include "admin/AKModel/render_ak_messages.html" %} - <a class="btn btn-danger" href="{% url 'admin:ak_delete_orga_messages' event_slug=event.slug %}">{% trans "Delete all messages" %}</a> - </div> - </div> - {% endtimezone %} -{% endblock %} diff --git a/AKModel/templates/admin/AKModel/status/event_aks.html b/AKModel/templates/admin/AKModel/status/event_aks.html new file mode 100644 index 0000000000000000000000000000000000000000..a99852ccea0a30b0b99b607ba4612e2350319b70 --- /dev/null +++ b/AKModel/templates/admin/AKModel/status/event_aks.html @@ -0,0 +1,28 @@ +{% load i18n %} +{% load tags_AKModel %} + +{% if event.ak_set.count == 0 %} + <p class="text-danger">{% trans "No AKs yet" %}</p> +{% else %} + <table> + <tbody> + <tr> + <td>{% trans "AKs" %}</td><td>{{ ak_count }}</td> + </tr> + <tr> + <td>{% trans "Slots" %}</td><td>{{ event.akslot_set.count }}</td> + </tr> + <tr> + <td>{% trans "Unscheduled Slots" %}</td><td> + {% if "AKScheduling"|check_app_installed %} + <a href="{% url 'admin:slots_unscheduled' event_slug=event.slug %}"> + {{ unscheduled_slots_count }} + </a> + {% else %} + {{ unscheduled_slots_count }} + {% endif %} + </td> + </tr> + </tbody> + </table> +{% endif %} diff --git a/AKModel/templates/admin/AKModel/status/event_categories.html b/AKModel/templates/admin/AKModel/status/event_categories.html new file mode 100644 index 0000000000000000000000000000000000000000..59c2a9ae2aaa4810b9e1333afe5a1baff013713f --- /dev/null +++ b/AKModel/templates/admin/AKModel/status/event_categories.html @@ -0,0 +1,14 @@ +{% load i18n %} + +{% if event.akcategory_set.count == 0 %} + <p class="text-danger">{% trans "No categories yet" %}</p> +{% else %} + <ul> + {% for category in event.akcategory_set.all %} + <li> + <a href="{% url 'admin:AKModel_akcategory_change' category.pk %}">{{ category }}</a> + ({{ category.ak_set.count }}) + </li> + {% endfor %} + </ul> +{% endif %} diff --git a/AKModel/templates/admin/AKModel/status/event_overview.html b/AKModel/templates/admin/AKModel/status/event_overview.html new file mode 100644 index 0000000000000000000000000000000000000000..6c0a7d1ee664caae662d96d7c6a796a0990d53d8 --- /dev/null +++ b/AKModel/templates/admin/AKModel/status/event_overview.html @@ -0,0 +1,14 @@ +{% load i18n %} +{% load tz %} + +{% timezone event.timezone %} + <h2><a href="{% url 'admin:AKModel_event_change' event.pk %}">{{event}}</a></h2> + <h5>{{ event.start }} - {{ event.end }}</h5> + + <div class="form-check form-switch mt-2 mb-2"> + <input type="checkbox" class="form-check-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="form-check-label" for="planPublishedSwitch">{% trans "Plan published?" %}</label> + </div> +{% endtimezone %} diff --git a/AKModel/templates/admin/AKModel/status/event_requirements.html b/AKModel/templates/admin/AKModel/status/event_requirements.html new file mode 100644 index 0000000000000000000000000000000000000000..f0e3b11fd83d0807cb38451fb3c0a91ef6b2f0c0 --- /dev/null +++ b/AKModel/templates/admin/AKModel/status/event_requirements.html @@ -0,0 +1,14 @@ +{% load i18n %} + +{% if event.akrequirement_set.count == 0 %} + <p class="text-danger">{% trans "No requirements yet" %}</p> +{% else %} + <ul> + {% for requirement in event.akrequirement_set.all %} + <li> + <a href="{% url 'admin:AKModel_akrequirement_change' requirement.pk %}">{{ requirement }}</a> + ({{ requirement.ak_set.count }}) + </li> + {% endfor %} + </ul> +{% endif %} diff --git a/AKModel/templates/admin/AKModel/status/event_rooms.html b/AKModel/templates/admin/AKModel/status/event_rooms.html new file mode 100644 index 0000000000000000000000000000000000000000..e58ecedda7e23c9bb1e8ae90610b6fc998c115d1 --- /dev/null +++ b/AKModel/templates/admin/AKModel/status/event_rooms.html @@ -0,0 +1,15 @@ +{% load i18n %} + +{% if event.room_set.count == 0 %} + <p class="text-danger">{% trans "No rooms yet" %}</p> +{% else %} + <p> + {% for room in event.room_set.all %} + {% if forloop.counter0 > 0 %} + · + {% endif %} + <a href="{% url 'admin:AKModel_room_change' room.pk %}">{{ room }}</a> + {% endfor %} + </p> +{% endif %} + diff --git a/AKModel/templates/admin/AKModel/status/status.html b/AKModel/templates/admin/AKModel/status/status.html new file mode 100644 index 0000000000000000000000000000000000000000..0a883762a7d699b475e3b49e1734ea2ad9a8bb18 --- /dev/null +++ b/AKModel/templates/admin/AKModel/status/status.html @@ -0,0 +1,40 @@ +{% extends "admin/base_site.html" %} +{% load tags_AKModel %} + +{% load i18n %} +{% load tz %} +{% load fontawesome_6 %} + +{% block title %}{% trans "Status" %}: {{ event }}{% endblock %} + +{% block content %} + {% timezone event.timezone %} + <div class="row"> + {% for widget in widgets %} + <div class="card border-{{ widget.status }} mb-3 me-2 col-xl-3 col-md-4 col-sm-6 p-0"> + <div class="card-header"> + {% if widget.actions %} + <div class="float-end"> + <a style="cursor: pointer;" data-bs-toggle="dropdown" aria-expanded="false"> + {% fa6_icon "ellipsis-vertical" %} + </a> + <ul class="dropdown-menu dropdown-menu-end"> + {% for action in widget.actions %} + <li class="dropdown-item"> + <a href="{{ action.url }}">{{ action.text }}</a> + </li> + {% endfor %} + </ul> + </div> + {% endif %} + {{ widget.title }} + </div> + <div class="card-body"> + {{ widget.body }} + </div> + </div> + {% endfor %} + </div> + + {% endtimezone %} +{% endblock %} diff --git a/AKModel/templates/admin/ak_index.html b/AKModel/templates/admin/ak_index.html index 6999744fe64ff631987fd47c28ab0da2b10bb09a..6b42403409e18e5ec71f1004a8bb1e90fce65c92 100644 --- a/AKModel/templates/admin/ak_index.html +++ b/AKModel/templates/admin/ak_index.html @@ -12,7 +12,7 @@ <a href="{% url 'admin:AKModel_event_change' event.pk %}">{{ event }}</a> ({{ event.start|timezone:event.timezone|date:"d.m.y" }} - {{ event.end|timezone:event.timezone|date:"d.m.y" }}) · - <a href="{% url 'admin:event_status' slug=event.slug %}">{% trans "Status" %}</a> · + <a href="{% url 'admin:event_status' event_slug=event.slug %}">{% trans "Status" %}</a> · <a href="{% url 'admin:schedule' event_slug=event.slug %}">{% trans "Scheduling" %}</a> </li> {% endfor %} diff --git a/AKModel/tests.py b/AKModel/tests.py index b6b490defe54760a7a05689a9bd3c4df8232ab93..d676992f93f21ef3d12fcdb7ba4556d0a34e6365 100644 --- a/AKModel/tests.py +++ b/AKModel/tests.py @@ -156,7 +156,7 @@ class ModelViewTests(BasicViewTests, TestCase): VIEWS_STAFF_ONLY = [ ('admin:index', {}), - ('admin:event_status', {'slug': 'kif42'}), + ('admin:event_status', {'event_slug': 'kif42'}), ('admin:event_requirement_overview', {'event_slug': 'kif42'}), ('admin:ak_csv_export', {'event_slug': 'kif42'}), ('admin:ak_wiki_export', {'slug': 'kif42'}), diff --git a/AKModel/urls.py b/AKModel/urls.py index 8113ee1b3a6acf549291b4fcdd3d67db3855a192..806178145462d507915f0d4c02d57462325d2255 100644 --- a/AKModel/urls.py +++ b/AKModel/urls.py @@ -3,18 +3,20 @@ from django.apps import apps from django.urls import include, path 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, ExportSlidesView +import AKModel.views.api +from AKModel.views.manage import ExportSlidesView +from AKModel.views.ak import AKRequirementOverview, AKCSVExportView, AKWikiExportView, AKMessageDeleteView +from AKModel.views.event_wizard import NewEventWizardStartView, NewEventWizardPrepareImportView, \ + NewEventWizardImportView, NewEventWizardActivateView, NewEventWizardFinishView, NewEventWizardSettingsView +from AKModel.views.status import EventStatusView api_router = DefaultRouter() -api_router.register('akowner', views.AKOwnerViewSet, basename='AKOwner') -api_router.register('akcategory', views.AKCategoryViewSet, basename='AKCategory') -api_router.register('aktrack', views.AKTrackViewSet, basename='AKTrack') -api_router.register('ak', views.AKViewSet, basename='AK') -api_router.register('room', views.RoomViewSet, basename='Room') -api_router.register('akslot', views.AKSlotViewSet, basename='AKSlot') +api_router.register('akowner', AKModel.views.api.AKOwnerViewSet, basename='AKOwner') +api_router.register('akcategory', AKModel.views.api.AKCategoryViewSet, basename='AKCategory') +api_router.register('aktrack', AKModel.views.api.AKTrackViewSet, basename='AKTrack') +api_router.register('ak', AKModel.views.api.AKViewSet, basename='AK') +api_router.register('room', AKModel.views.api.RoomViewSet, basename='Room') +api_router.register('akslot', AKModel.views.api.AKSlotViewSet, basename='AKSlot') extra_paths = [] if apps.is_installed("AKScheduling"): @@ -48,7 +50,7 @@ urlpatterns = [ '<slug:event_slug>/', include(event_specific_paths) ), - path('user/', views.UserView.as_view(), name="user"), + path('user/', AKModel.views.manage.UserView.as_view(), name="user"), ] @@ -74,7 +76,7 @@ def get_admin_urls_event_wizard(admin_site): def get_admin_urls_event(admin_site): return [ - path('<slug:slug>/status/', admin_site.admin_view(EventStatusView.as_view()), name="event_status"), + path('<slug:event_slug>/status/', admin_site.admin_view(EventStatusView.as_view()), name="event_status"), path('<slug:event_slug>/requirements/', admin_site.admin_view(AKRequirementOverview.as_view()), name="event_requirement_overview"), path('<slug:event_slug>/ak-csv-export/', admin_site.admin_view(AKCSVExportView.as_view()), diff --git a/AKModel/views.py b/AKModel/views.py deleted file mode 100644 index 3d4e56320752caf9be34fb8874326a587ea48a74..0000000000000000000000000000000000000000 --- a/AKModel/views.py +++ /dev/null @@ -1,651 +0,0 @@ -import csv -import datetime -import json -import os -import tempfile -from abc import ABC, abstractmethod -from itertools import zip_longest - -import django.db -from django.apps import apps -from django.contrib import admin, messages -from django.db.models.functions import Now -from django.http import HttpResponseRedirect -from django.shortcuts import get_object_or_404, redirect -from django.urls import reverse_lazy, reverse -from django.utils.dateparse import parse_datetime -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 -from django_tex.response import PDFResponse -from rest_framework import viewsets, permissions, mixins - -from AKModel.forms import NewEventWizardStartForm, NewEventWizardSettingsForm, NewEventWizardPrepareImportForm, \ - NewEventWizardImportForm, NewEventWizardActivateForm, AdminIntermediateForm, SlideExportForm, \ - AdminIntermediateActionForm, DefaultSlotEditorForm, RoomBatchCreationForm, RoomForm -from AKModel.models import Event, AK, AKSlot, Room, AKTrack, AKCategory, AKOwner, AKOrgaMessage, AKRequirement, \ - ConstraintViolation, DefaultSlot -from AKModel.serializers import AKSerializer, AKSlotSerializer, RoomSerializer, AKTrackSerializer, AKCategorySerializer, \ - AKOwnerSerializer - - -class EventSlugMixin: - """ - Mixin to handle views with event slugs - """ - event = None - - def _load_event(self): - # Find event based on event slug - if self.event is None: - self.event = get_object_or_404(Event, slug=self.kwargs.get("event_slug", None)) - - def get(self, request, *args, **kwargs): - self._load_event() - return super().get(request, *args, **kwargs) - - def post(self, request, *args, **kwargs): - self._load_event() - return super().post(request, *args, **kwargs) - - def list(self, request, *args, **kwargs): - self._load_event() - return super().list(request, *args, **kwargs) - - def create(self, request, *args, **kwargs): - self._load_event() - return super().create(request, *args, **kwargs) - - def dispatch(self, request, *args, **kwargs): - if self.event is None: - self._load_event() - return super().dispatch(request, *args, **kwargs) - - def get_context_data(self, *, object_list=None, **kwargs): - context = super().get_context_data(object_list=object_list, **kwargs) - # Add event to context (to make it accessible in templates) - context["event"] = self.event - return context - - -class FilterByEventSlugMixin(EventSlugMixin): - """ - Mixin to filter different querysets based on a event slug from the request url - """ - - def get_queryset(self): - # Filter current queryset based on url event slug or return 404 if event slug is invalid - return super().get_queryset().filter(event=self.event) - - -class AdminViewMixin: - site_url = '' - title = '' - - def get_context_data(self, **kwargs): - extra = admin.site.each_context(self.request) - extra.update(super().get_context_data(**kwargs)) - - if self.site_url != '': - extra["site_url"] = self.site_url - if self.title != '': - extra["title"] = self.title - - return extra - - -class AKOwnerViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet): - permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,) - serializer_class = AKOwnerSerializer - - def get_queryset(self): - return AKOwner.objects.filter(event=self.event) - - -class AKCategoryViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet): - permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,) - serializer_class = AKCategorySerializer - - def get_queryset(self): - return AKCategory.objects.filter(event=self.event) - - -class AKTrackViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin, mixins.UpdateModelMixin, - mixins.DestroyModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet): - permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,) - serializer_class = AKTrackSerializer - - def get_queryset(self): - return AKTrack.objects.filter(event=self.event) - - -class AKViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.ListModelMixin, - viewsets.GenericViewSet): - permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,) - serializer_class = AKSerializer - - def get_queryset(self): - return AK.objects.filter(event=self.event) - - -class RoomViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet): - permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,) - serializer_class = RoomSerializer - - def get_queryset(self): - return Room.objects.filter(event=self.event) - - -class AKSlotViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin, mixins.UpdateModelMixin, - mixins.ListModelMixin, viewsets.GenericViewSet): - permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,) - serializer_class = AKSlotSerializer - - def get_queryset(self): - return AKSlot.objects.filter(event=self.event) - - -class UserView(TemplateView): - template_name = "AKModel/user.html" - - -class EventStatusView(AdminViewMixin, DetailView): - template_name = "admin/AKModel/status.html" - model = Event - context_object_name = "event" - title = _("Event Status") - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["unscheduled_slots_count"] = context["event"].akslot_set.filter(start=None).count - context["site_url"] = reverse_lazy("dashboard:dashboard_event", kwargs={'slug': context["event"].slug}) - context["ak_messages"] = AKOrgaMessage.objects.filter(ak__event=context["event"]) - return context - - -class AKRequirementOverview(AdminViewMixin, FilterByEventSlugMixin, ListView): - model = AKRequirement - context_object_name = "requirements" - title = _("Requirements for Event") - template_name = "admin/AKModel/requirements_overview.html" - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["event"] = self.event - context["site_url"] = reverse_lazy("dashboard:dashboard_event", kwargs={'slug': context["event"].slug}) - return context - - -class AKCSVExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): - template_name = "admin/AKModel/ak_csv_export.html" - model = AKSlot - context_object_name = "slots" - title = _("AK CSV Export") - - def get_queryset(self): - return super().get_queryset().order_by("ak__track") - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - return context - - -class AKWikiExportView(AdminViewMixin, DetailView): - template_name = "admin/AKModel/wiki_export.html" - model = Event - context_object_name = "event" - title = _("AK Wiki Export") - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - - categories_with_aks, ak_wishes = context["event"].get_categories_with_aks( - wishes_seperately=True, - filter=lambda ak: ak.include_in_export - ) - - context["categories_with_aks"] = [(category.name, ak_list) for category, ak_list in categories_with_aks] - context["categories_with_aks"].append((_("Wishes"), ak_wishes)) - - return context - - -class IntermediateAdminView(AdminViewMixin, FormView): - template_name = "admin/AKModel/action_intermediate.html" - form_class = AdminIntermediateForm - - def get_preview(self): - return "" - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["title"] = self.title - context["preview"] = self.get_preview() - return context - - -class AKMessageDeleteView(EventSlugMixin, IntermediateAdminView): - template_name = "admin/AKModel/message_delete.html" - title = _("Delete AK Orga Messages") - - def get_orga_messages_for_event(self, event): - return AKOrgaMessage.objects.filter(ak__event=event) - - def get_success_url(self): - return reverse_lazy('admin:event_status', kwargs={'slug': self.event.slug}) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["ak_messages"] = self.get_orga_messages_for_event(self.event) - return context - - def form_valid(self, form): - self.get_orga_messages_for_event(self.event).delete() - messages.add_message(self.request, messages.SUCCESS, _("AK Orga Messages successfully deleted")) - return super().form_valid(form) - - -class WizardViewMixin: - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["wizard_step"] = self.wizard_step - context["wizard_steps"] = [ - _("Start"), - _("Settings"), - _("Event created, Prepare Import"), - _("Import categories & requirements"), - _("Activate?"), - _("Finish") - ] - context["wizard_step_text"] = context["wizard_steps"][self.wizard_step - 1] - context["wizard_steps_total"] = len(context["wizard_steps"]) - return context - - -class NewEventWizardStartView(AdminViewMixin, WizardViewMixin, CreateView): - model = Event - form_class = NewEventWizardStartForm - template_name = "admin/AKModel/event_wizard/start.html" - wizard_step = 1 - - -class NewEventWizardSettingsView(AdminViewMixin, WizardViewMixin, CreateView): - model = Event - form_class = NewEventWizardSettingsForm - template_name = "admin/AKModel/event_wizard/settings.html" - wizard_step = 2 - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["timezone"] = context["form"].cleaned_data["timezone"] - return context - - def get_success_url(self): - return reverse_lazy("admin:new_event_wizard_prepare_import", kwargs={"event_slug": self.object.slug}) - - -class NewEventWizardPrepareImportView(WizardViewMixin, EventSlugMixin, FormView): - form_class = NewEventWizardPrepareImportForm - template_name = "admin/AKModel/event_wizard/created_prepare_import.html" - wizard_step = 3 - - def form_valid(self, form): - # Selected a valid event to import from? Use this to go to next step of wizard - return redirect("admin:new_event_wizard_import", event_slug=self.event.slug, - import_slug=form.cleaned_data["import_event"].slug) - - -class NewEventWizardImportView(EventSlugMixin, WizardViewMixin, FormView): - form_class = NewEventWizardImportForm - template_name = "admin/AKModel/event_wizard/import.html" - wizard_step = 4 - - def get_initial(self): - initial = super().get_initial() - initial["import_event"] = Event.objects.get(slug=self.kwargs["import_slug"]) - return initial - - def form_valid(self, form): - import_types = ["import_categories", "import_requirements"] - if apps.is_installed("AKDashboard"): - import_types.append("import_buttons") - - for import_type in import_types: - for import_obj in form.cleaned_data.get(import_type): - # clone existing entry - try: - import_obj.event = self.event - import_obj.pk = None - import_obj.save() - messages.add_message(self.request, messages.SUCCESS, _("Copied '%(obj)s'" % {'obj': import_obj})) - except BaseException as e: - messages.add_message(self.request, messages.ERROR, - _("Could not copy '%(obj)s' (%(error)s)" % {'obj': import_obj, - "error": str(e)})) - return redirect("admin:new_event_wizard_activate", slug=self.event.slug) - - -class NewEventWizardActivateView(WizardViewMixin, UpdateView): - model = Event - template_name = "admin/AKModel/event_wizard/activate.html" - form_class = NewEventWizardActivateForm - wizard_step = 5 - - def get_success_url(self): - return reverse_lazy("admin:new_event_wizard_finish", kwargs={"slug": self.object.slug}) - - -class NewEventWizardFinishView(WizardViewMixin, DetailView): - model = Event - template_name = "admin/AKModel/event_wizard/finish.html" - wizard_step = 6 - - -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) - - timestamp = datetime.datetime.now(tz=self.event.timezone).strftime("%Y-%m-%d_%H_%M") - return PDFResponse(pdf, filename=f'{self.event.slug}_ak_slides_{timestamp}.pdf') - - -class IntermediateAdminActionView(IntermediateAdminView, ABC): - form_class = AdminIntermediateActionForm - entities = None - - 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): - 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 action(self, form): - pass - - def form_valid(self, form): - 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 action(self, form): - self.entities.update(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 action(self, form): - self.entities.update(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 action(self, form): - self.entities.update(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 action(self, form): - self.entities.update(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 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) - - -class DefaultSlotEditorView(EventSlugMixin, IntermediateAdminView): - template_name = "admin/AKModel/default_slot_editor.html" - form_class = DefaultSlotEditorForm - title = _("Edit Default Slots") - - def get_success_url(self): - return self.request.path - - def get_initial(self): - initial = super().get_initial() - default_slots = [ - {"id": s.id, "start": s.start_iso, "end": s.end_iso, "allDay": False} - for s in self.event.defaultslot_set.all() - ] - initial['availabilities'] = json.dumps({ - 'availabilities': default_slots - }) - return initial - - def form_valid(self, form): - default_slots_raw = json.loads(form.cleaned_data['availabilities'])["availabilities"] - tz = self.event.timezone - - created_count = 0 - updated_count = 0 - - previous_slot_ids = set(s.id for s in self.event.defaultslot_set.all()) - - for slot in default_slots_raw: - start = parse_datetime(slot["start"]).astimezone(tz) - end = parse_datetime(slot["end"]).astimezone(tz) - - if slot["id"] != '': - id = int(slot["id"]) - if id not in previous_slot_ids: - # Make sure only slots (currently) belonging to this event are edited - # (user did not manipulate IDs and slots have not been deleted in another session in the meantime) - messages.add_message( - self.request, - messages.WARNING, - _("Could not update slot {id} since it does not belong to {event}") - .format(id=slot['id'], event=self.event.name) - ) - else: - # Update existing entries - previous_slot_ids.remove(id) - original_slot = DefaultSlot.objects.get(id=id) - if original_slot.start != start or original_slot.end != end: - original_slot.start = start - original_slot.end = end - original_slot.save() - updated_count += 1 - else: - # Create new entries - DefaultSlot.objects.create( - start=start, - end=end, - event=self.event - ) - created_count += 1 - - # Delete all slots not re-submitted by the user (and hence deleted in editor) - deleted_count = len(previous_slot_ids) - for d_id in previous_slot_ids: - DefaultSlot.objects.get(id=d_id).delete() - - if created_count + updated_count + deleted_count > 0: - messages.add_message( - self.request, - messages.SUCCESS, - _("Updated {u} slot(s). created {c} new slot(s) and deleted {d} slot(s)") - .format(u=str(updated_count), c=str(created_count), d=str(deleted_count)) - ) - return super().form_valid(form) - - -class RoomCreationView(AdminViewMixin, CreateView): - form_class = RoomForm - template_name = 'admin/AKModel/room_create.html' - - def get_success_url(self): - print(self.request.POST['save_action']) - if self.request.POST['save_action'] == 'save_add_another': - return reverse_lazy('admin:room-new') - elif self.request.POST['save_action'] == 'save_continue': - return reverse_lazy('admin:AKModel_room_change', kwargs={'object_id': self.room.pk}) - else: - return reverse_lazy('admin:AKModel_room_changelist') - - def form_valid(self, form): - self.room = form.save() - messages.success(self.request, _("Created Room '%(room)s'" % {'room': self.room})) - return HttpResponseRedirect(self.get_success_url()) - - -class RoomBatchCreationView(EventSlugMixin, IntermediateAdminView): - form_class = RoomBatchCreationForm - title = _("Import Rooms from CSV") - - def get_success_url(self): - return reverse_lazy('admin:event_status', kwargs={'slug': self.event.slug}) - - def form_valid(self, form): - virtual_rooms_support = False - created_count = 0 - - rooms_raw_dict: csv.DictReader = form.cleaned_data["rooms"] - - if apps.is_installed("AKOnline") and "url" in rooms_raw_dict.fieldnames: - virtual_rooms_support = True - from AKOnline.models import VirtualRoom - - for raw_room in rooms_raw_dict: - name = raw_room["name"] - location = raw_room["location"] if "location" in rooms_raw_dict.fieldnames else "" - capacity = raw_room["capacity"] if "capacity" in rooms_raw_dict.fieldnames else -1 - - try: - r = Room.objects.create(name=name, - location=location, - capacity=capacity, - event=self.event) - if virtual_rooms_support and raw_room["url"] != "": - VirtualRoom.objects.create(room=r, - url=raw_room["url"]) - created_count += 1 - except django.db.Error as e: - messages.add_message(self.request, messages.WARNING, - _("Could not import room {name}: {e}").format(name=name, e=str(e))) - - if created_count > 0: - messages.add_message(self.request, messages.SUCCESS, - _("Imported {count} room(s)").format(count=created_count)) - else: - messages.add_message(self.request, messages.WARNING, _("No rooms imported")) - return super().form_valid(form) diff --git a/AKModel/views/__init__.py b/AKModel/views/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py new file mode 100644 index 0000000000000000000000000000000000000000..640d398ee98119407035a4106df3f9ec0e1baf27 --- /dev/null +++ b/AKModel/views/ak.py @@ -0,0 +1,96 @@ +from django.contrib import messages +from django.urls import reverse_lazy +from django.utils.translation import gettext_lazy as _ +from django.views.generic import ListView, DetailView + +from AKModel.metaviews.admin import AdminViewMixin, FilterByEventSlugMixin, EventSlugMixin, IntermediateAdminView, \ + IntermediateAdminActionView +from AKModel.models import AKRequirement, AKSlot, Event, AKOrgaMessage, AK + + +class AKRequirementOverview(AdminViewMixin, FilterByEventSlugMixin, ListView): + model = AKRequirement + context_object_name = "requirements" + title = _("Requirements for Event") + template_name = "admin/AKModel/requirements_overview.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["event"] = self.event + context["site_url"] = reverse_lazy("dashboard:dashboard_event", kwargs={'slug': context["event"].slug}) + return context + + +class AKCSVExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): + template_name = "admin/AKModel/ak_csv_export.html" + model = AKSlot + context_object_name = "slots" + title = _("AK CSV Export") + + def get_queryset(self): + return super().get_queryset().order_by("ak__track") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + return context + + +class AKWikiExportView(AdminViewMixin, DetailView): + template_name = "admin/AKModel/wiki_export.html" + model = Event + context_object_name = "event" + title = _("AK Wiki Export") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + categories_with_aks, ak_wishes = context["event"].get_categories_with_aks( + wishes_seperately=True, + filter=lambda ak: ak.include_in_export + ) + + context["categories_with_aks"] = [(category.name, ak_list) for category, ak_list in categories_with_aks] + context["categories_with_aks"].append((_("Wishes"), ak_wishes)) + + return context + + +class AKMessageDeleteView(EventSlugMixin, IntermediateAdminView): + template_name = "admin/AKModel/message_delete.html" + title = _("Delete AK Orga Messages") + + def get_orga_messages_for_event(self, event): + return AKOrgaMessage.objects.filter(ak__event=event) + + def get_success_url(self): + return reverse_lazy('admin:event_status', kwargs={'slug': self.event.slug}) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["ak_messages"] = self.get_orga_messages_for_event(self.event) + return context + + def form_valid(self, form): + self.get_orga_messages_for_event(self.event).delete() + messages.add_message(self.request, messages.SUCCESS, _("AK Orga Messages successfully deleted")) + return super().form_valid(form) + + +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 action(self, form): + self.entities.update(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 action(self, form): + self.entities.update(interest_counter=0) diff --git a/AKModel/views/api.py b/AKModel/views/api.py new file mode 100644 index 0000000000000000000000000000000000000000..abf4c261e745b06e343388b47de68acba3f9da4b --- /dev/null +++ b/AKModel/views/api.py @@ -0,0 +1,57 @@ +from rest_framework import mixins, viewsets, permissions + +from AKModel.metaviews.admin import EventSlugMixin +from AKModel.models import AKOwner, AKCategory, AKTrack, AK, Room, AKSlot +from AKModel.serializers import AKOwnerSerializer, AKCategorySerializer, AKTrackSerializer, AKSerializer, \ + RoomSerializer, AKSlotSerializer + + +class AKOwnerViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet): + permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,) + serializer_class = AKOwnerSerializer + + def get_queryset(self): + return AKOwner.objects.filter(event=self.event) + + +class AKCategoryViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet): + permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,) + serializer_class = AKCategorySerializer + + def get_queryset(self): + return AKCategory.objects.filter(event=self.event) + + +class AKTrackViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin, mixins.UpdateModelMixin, + mixins.DestroyModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet): + permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,) + serializer_class = AKTrackSerializer + + def get_queryset(self): + return AKTrack.objects.filter(event=self.event) + + +class AKViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.ListModelMixin, + viewsets.GenericViewSet): + permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,) + serializer_class = AKSerializer + + def get_queryset(self): + return AK.objects.filter(event=self.event) + + +class RoomViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet): + permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,) + serializer_class = RoomSerializer + + def get_queryset(self): + return Room.objects.filter(event=self.event) + + +class AKSlotViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin, mixins.UpdateModelMixin, + mixins.ListModelMixin, viewsets.GenericViewSet): + permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,) + serializer_class = AKSlotSerializer + + def get_queryset(self): + return AKSlot.objects.filter(event=self.event) diff --git a/AKModel/views/event_wizard.py b/AKModel/views/event_wizard.py new file mode 100644 index 0000000000000000000000000000000000000000..2aca36607af304fe6d21ea6c01eaa55fc081a736 --- /dev/null +++ b/AKModel/views/event_wizard.py @@ -0,0 +1,90 @@ +from django.apps import apps +from django.contrib import messages +from django.shortcuts import redirect +from django.urls import reverse_lazy +from django.utils.translation import gettext_lazy as _ +from django.views.generic import CreateView, FormView, UpdateView, DetailView + +from AKModel.forms import NewEventWizardStartForm, NewEventWizardSettingsForm, NewEventWizardPrepareImportForm, \ + NewEventWizardImportForm, NewEventWizardActivateForm +from AKModel.metaviews.admin import AdminViewMixin, WizardViewMixin, EventSlugMixin +from AKModel.models import Event + + +class NewEventWizardStartView(AdminViewMixin, WizardViewMixin, CreateView): + model = Event + form_class = NewEventWizardStartForm + template_name = "admin/AKModel/event_wizard/start.html" + wizard_step = 1 + + +class NewEventWizardSettingsView(AdminViewMixin, WizardViewMixin, CreateView): + model = Event + form_class = NewEventWizardSettingsForm + template_name = "admin/AKModel/event_wizard/settings.html" + wizard_step = 2 + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["timezone"] = context["form"].cleaned_data["timezone"] + return context + + def get_success_url(self): + return reverse_lazy("admin:new_event_wizard_prepare_import", kwargs={"event_slug": self.object.slug}) + + +class NewEventWizardPrepareImportView(WizardViewMixin, EventSlugMixin, FormView): + form_class = NewEventWizardPrepareImportForm + template_name = "admin/AKModel/event_wizard/created_prepare_import.html" + wizard_step = 3 + + def form_valid(self, form): + # Selected a valid event to import from? Use this to go to next step of wizard + return redirect("admin:new_event_wizard_import", event_slug=self.event.slug, + import_slug=form.cleaned_data["import_event"].slug) + + +class NewEventWizardImportView(EventSlugMixin, WizardViewMixin, FormView): + form_class = NewEventWizardImportForm + template_name = "admin/AKModel/event_wizard/import.html" + wizard_step = 4 + + def get_initial(self): + initial = super().get_initial() + initial["import_event"] = Event.objects.get(slug=self.kwargs["import_slug"]) + return initial + + def form_valid(self, form): + import_types = ["import_categories", "import_requirements"] + if apps.is_installed("AKDashboard"): + import_types.append("import_buttons") + + for import_type in import_types: + for import_obj in form.cleaned_data.get(import_type): + # clone existing entry + try: + import_obj.event = self.event + import_obj.pk = None + import_obj.save() + messages.add_message(self.request, messages.SUCCESS, _("Copied '%(obj)s'" % {'obj': import_obj})) + except BaseException as e: + messages.add_message(self.request, messages.ERROR, + _("Could not copy '%(obj)s' (%(error)s)" % {'obj': import_obj, + "error": str(e)})) + return redirect("admin:new_event_wizard_activate", slug=self.event.slug) + + +class NewEventWizardActivateView(WizardViewMixin, UpdateView): + model = Event + template_name = "admin/AKModel/event_wizard/activate.html" + form_class = NewEventWizardActivateForm + wizard_step = 5 + + def get_success_url(self): + return reverse_lazy("admin:new_event_wizard_finish", kwargs={"slug": self.object.slug}) + + +class NewEventWizardFinishView(WizardViewMixin, DetailView): + model = Event + template_name = "admin/AKModel/event_wizard/finish.html" + wizard_step = 6 diff --git a/AKModel/views/manage.py b/AKModel/views/manage.py new file mode 100644 index 0000000000000000000000000000000000000000..e4c7250381e6ef721f31ef568431dd9fe22c109c --- /dev/null +++ b/AKModel/views/manage.py @@ -0,0 +1,197 @@ +import datetime +import json +import os +import tempfile +from itertools import zip_longest + +from django.contrib import messages +from django.db.models.functions import Now +from django.utils.dateparse import parse_datetime +from django.utils.translation import gettext_lazy as _ +from django.views.generic import TemplateView +from django_tex.core import render_template_with_context, run_tex_in_directory +from django_tex.response import PDFResponse + +from AKModel.forms import SlideExportForm, DefaultSlotEditorForm +from AKModel.metaviews.admin import EventSlugMixin, IntermediateAdminView, IntermediateAdminActionView +from AKModel.models import ConstraintViolation, Event, DefaultSlot + + +class UserView(TemplateView): + template_name = "AKModel/user.html" + + +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) + + timestamp = datetime.datetime.now(tz=self.event.timezone).strftime("%Y-%m-%d_%H_%M") + return PDFResponse(pdf, filename=f'{self.event.slug}_ak_slides_{timestamp}.pdf') + + +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 action(self, form): + self.entities.update(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 action(self, form): + self.entities.update(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 action(self, form): + self.entities.update(level=ConstraintViolation.ViolationLevel.WARNING) + + +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) + + +class DefaultSlotEditorView(EventSlugMixin, IntermediateAdminView): + template_name = "admin/AKModel/default_slot_editor.html" + form_class = DefaultSlotEditorForm + title = _("Edit Default Slots") + + def get_success_url(self): + return self.request.path + + def get_initial(self): + initial = super().get_initial() + default_slots = [ + {"id": s.id, "start": s.start_iso, "end": s.end_iso, "allDay": False} + for s in self.event.defaultslot_set.all() + ] + initial['availabilities'] = json.dumps({ + 'availabilities': default_slots + }) + return initial + + def form_valid(self, form): + default_slots_raw = json.loads(form.cleaned_data['availabilities'])["availabilities"] + tz = self.event.timezone + + created_count = 0 + updated_count = 0 + + previous_slot_ids = set(s.id for s in self.event.defaultslot_set.all()) + + for slot in default_slots_raw: + start = parse_datetime(slot["start"]).astimezone(tz) + end = parse_datetime(slot["end"]).astimezone(tz) + + if slot["id"] != '': + id = int(slot["id"]) + if id not in previous_slot_ids: + # Make sure only slots (currently) belonging to this event are edited + # (user did not manipulate IDs and slots have not been deleted in another session in the meantime) + messages.add_message( + self.request, + messages.WARNING, + _("Could not update slot {id} since it does not belong to {event}") + .format(id=slot['id'], event=self.event.name) + ) + else: + # Update existing entries + previous_slot_ids.remove(id) + original_slot = DefaultSlot.objects.get(id=id) + if original_slot.start != start or original_slot.end != end: + original_slot.start = start + original_slot.end = end + original_slot.save() + updated_count += 1 + else: + # Create new entries + DefaultSlot.objects.create( + start=start, + end=end, + event=self.event + ) + created_count += 1 + + # Delete all slots not re-submitted by the user (and hence deleted in editor) + deleted_count = len(previous_slot_ids) + for d_id in previous_slot_ids: + DefaultSlot.objects.get(id=d_id).delete() + + if created_count + updated_count + deleted_count > 0: + messages.add_message( + self.request, + messages.SUCCESS, + _("Updated {u} slot(s). created {c} new slot(s) and deleted {d} slot(s)") + .format(u=str(updated_count), c=str(created_count), d=str(deleted_count)) + ) + return super().form_valid(form) diff --git a/AKModel/views/room.py b/AKModel/views/room.py new file mode 100644 index 0000000000000000000000000000000000000000..94c16b89608e486866d32535147c93f4da00c0ce --- /dev/null +++ b/AKModel/views/room.py @@ -0,0 +1,75 @@ +import csv + +import django.db +from django.apps import apps +from django.contrib import messages +from django.http import HttpResponseRedirect +from django.urls import reverse_lazy +from django.utils.translation import gettext_lazy as _ +from django.views.generic import CreateView + +from AKModel.forms import RoomForm, RoomBatchCreationForm +from AKModel.metaviews.admin import AdminViewMixin, EventSlugMixin, IntermediateAdminView +from AKModel.models import Room + + +class RoomCreationView(AdminViewMixin, CreateView): + form_class = RoomForm + template_name = 'admin/AKModel/room_create.html' + + def get_success_url(self): + print(self.request.POST['save_action']) + if self.request.POST['save_action'] == 'save_add_another': + return reverse_lazy('admin:room-new') + elif self.request.POST['save_action'] == 'save_continue': + return reverse_lazy('admin:AKModel_room_change', kwargs={'object_id': self.room.pk}) + else: + return reverse_lazy('admin:AKModel_room_changelist') + + def form_valid(self, form): + self.room = form.save() + messages.success(self.request, _("Created Room '%(room)s'" % {'room': self.room})) + return HttpResponseRedirect(self.get_success_url()) + + +class RoomBatchCreationView(EventSlugMixin, IntermediateAdminView): + form_class = RoomBatchCreationForm + title = _("Import Rooms from CSV") + + def get_success_url(self): + return reverse_lazy('admin:event_status', kwargs={'slug': self.event.slug}) + + def form_valid(self, form): + virtual_rooms_support = False + created_count = 0 + + rooms_raw_dict: csv.DictReader = form.cleaned_data["rooms"] + + if apps.is_installed("AKOnline") and "url" in rooms_raw_dict.fieldnames: + virtual_rooms_support = True + from AKOnline.models import VirtualRoom + + for raw_room in rooms_raw_dict: + name = raw_room["name"] + location = raw_room["location"] if "location" in rooms_raw_dict.fieldnames else "" + capacity = raw_room["capacity"] if "capacity" in rooms_raw_dict.fieldnames else -1 + + try: + r = Room.objects.create(name=name, + location=location, + capacity=capacity, + event=self.event) + if virtual_rooms_support and raw_room["url"] != "": + VirtualRoom.objects.create(room=r, + url=raw_room["url"]) + created_count += 1 + except django.db.Error as e: + messages.add_message(self.request, messages.WARNING, + _("Could not import room {name}: {e}").format(name=name, e=str(e))) + + if created_count > 0: + messages.add_message(self.request, messages.SUCCESS, + _("Imported {count} room(s)").format(count=created_count)) + else: + messages.add_message(self.request, messages.WARNING, _("No rooms imported")) + return super().form_valid(form) diff --git a/AKModel/views/status.py b/AKModel/views/status.py new file mode 100644 index 0000000000000000000000000000000000000000..460c13afaf42ea3ab0b2bfd1f7577f2070182625 --- /dev/null +++ b/AKModel/views/status.py @@ -0,0 +1,163 @@ +from django.apps import apps +from django.urls import reverse_lazy +from django.utils.html import format_html +from django.utils.translation import gettext_lazy as _ + +from AKModel.metaviews import status_manager +from AKModel.metaviews.admin import EventSlugMixin, AdminViewMixin +from AKModel.metaviews.status import TemplateStatusWidget, StatusView + + +@status_manager.register(name="event_overview") +class EventOverviewWidget(TemplateStatusWidget): + required_context_type = "event" + title = _("Overview") + template_name = "admin/AKModel/status/event_overview.html" + + def render_status(self, context: {}) -> str: + return "success" if not context["event"].plan_hidden else "primary" + + +@status_manager.register(name="event_categories") +class EventCategoriesWidget(TemplateStatusWidget): + required_context_type = "event" + title = _("Categories") + template_name = "admin/AKModel/status/event_categories.html" + actions = [ + { + "text": _("Add category"), + "url": reverse_lazy("admin:AKModel_akcategory_add"), + } + ] + + def render_title(self, context: {}) -> str: + self.category_count = context['event'].akcategory_set.count() + return f"{super().render_title(context)} ({self.category_count})" + + def render_status(self, context: {}) -> str: + return "danger" if self.category_count == 0 else "primary" + + +@status_manager.register(name="event_rooms") +class EventRoomsWidget(TemplateStatusWidget): + required_context_type = "event" + title = _("Rooms") + template_name = "admin/AKModel/status/event_rooms.html" + actions = [ + { + "text": _("Add Room"), + "url": reverse_lazy("admin:AKModel_room_add"), + } + ] + + def render_title(self, context: {}) -> str: + self.room_count = context['event'].room_set.count() + return f"{super().render_title(context)} ({self.room_count})" + + def render_status(self, context: {}) -> str: + return "danger" if self.room_count == 0 else "primary" + + def render_actions(self, context: {}) -> list[dict]: + actions = super().render_actions(context) + actions.append( + { + "text": _("Import Rooms from CSV"), + "url": reverse_lazy("admin:room-import", kwargs={"event_slug": context["event"].slug}), + } + ) + return actions + + +@status_manager.register(name="event_aks") +class EventAKsWidget(TemplateStatusWidget): + required_context_type = "event" + title = _("AKs") + template_name = "admin/AKModel/status/event_aks.html" + + def get_context_data(self, context) -> dict: + context["ak_count"] = context["event"].ak_set.count() + context["unscheduled_slots_count"] = context["event"].akslot_set.filter(start=None).count + return context + + def render_actions(self, context: {}) -> list[dict]: + actions = [ + { + "text": _("Scheduling"), + "url": reverse_lazy("admin:schedule", kwargs={"event_slug": context["event"].slug}), + }, + ] + if apps.is_installed("AKScheduling"): + actions.extend([ + { + "text": format_html('{} <span class="badge bg-secondary">{}</span>', + _("Constraint Violations"), + context["event"].constraintviolation_set.count()), + "url": reverse_lazy("admin:constraint-violations", kwargs={"slug": context["event"].slug}), + }, + { + "text": _("AKs requiring special attention"), + "url": reverse_lazy("admin:special-attention", kwargs={"slug": context["event"].slug}), + }, + { + "text": _("Enter Interest"), + "url": reverse_lazy("admin:enter-interest", + kwargs={"event_slug": context["event"].slug, "pk": context["event"].ak_set.all().first().pk}), + }, + ]) + actions.extend([ + { + "text": _("Edit Default Slots"), + "url": reverse_lazy("admin:default-slots-editor", kwargs={"event_slug": context["event"].slug}), + }, + { + "text": _("Manage ak tracks"), + "url": reverse_lazy("admin:tracks_manage", kwargs={"event_slug": context["event"].slug}), + }, + { + "text": _("Export AKs as CSV"), + "url": reverse_lazy("admin:ak_csv_export", kwargs={"event_slug": context["event"].slug}), + }, + { + "text": _("Export AKs for Wiki"), + "url": reverse_lazy("admin:ak_wiki_export", kwargs={"slug": context["event"].slug}), + }, + { + "text": _("Export AK Slides"), + "url": reverse_lazy("admin:ak_slide_export", kwargs={"event_slug": context["event"].slug}), + }, + ] + ) + return actions + + +@status_manager.register(name="event_requirements") +class EventRequirementsWidget(TemplateStatusWidget): + required_context_type = "event" + title = _("Requirements") + template_name = "admin/AKModel/status/event_requirements.html" + + def render_title(self, context: {}) -> str: + self.requirements_count = context['event'].akrequirement_set.count() + return f"{super().render_title(context)} ({self.requirements_count})" + + def render_actions(self, context: {}) -> list[dict]: + return [ + { + "text": _("Show AKs for requirements"), + "url": reverse_lazy("admin:event_requirement_overview", kwargs={"event_slug": context["event"].slug}), + }, + { + "text": _("Add Requirement"), + "url": reverse_lazy("admin:AKModel_akrequirement_add"), + }, + ] + + +class EventStatusView(EventSlugMixin, StatusView): + title = _("Event Status") + provided_context_type = "event" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["site_url"] = reverse_lazy("dashboard:dashboard_event", kwargs={'slug': context["event"].slug}) + return context diff --git a/AKOnline/locale/de_DE/LC_MESSAGES/django.po b/AKOnline/locale/de_DE/LC_MESSAGES/django.po index 94047231f59ee7d730d6385199930514f73e2239..cf4c442eb4ba431a086fe3c0f9ad748c628ed3f7 100644 --- a/AKOnline/locale/de_DE/LC_MESSAGES/django.po +++ b/AKOnline/locale/de_DE/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-03-24 18:01+0100\n" +"POT-Creation-Date: 2023-03-26 19:51+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" @@ -34,7 +34,7 @@ msgstr "Raum" msgid "Virtual Room" msgstr "Virtueller Raum" -#: AKOnline/models.py:17 +#: AKOnline/models.py:17 AKOnline/views.py:27 msgid "Virtual Rooms" msgstr "Virtuelle Räume" @@ -42,12 +42,12 @@ msgstr "Virtuelle Räume" msgid "Leave empty if that room is not virtual/hybrid." msgstr "Leer lassen wenn der Raum nicht virtuell/hybrid ist" -#: AKOnline/views.py:16 +#: AKOnline/views.py:18 #, python-format msgid "Created Room '%(room)s'" msgstr "Raum '%(room)s' angelegt" -#: AKOnline/views.py:18 +#: AKOnline/views.py:20 #, python-format msgid "Created related Virtual Room '%(vroom)s'" msgstr "Verbundenen virtuellen Raum '%(vroom)s' angelegt" diff --git a/AKOnline/templates/admin/AKOnline/status/event_virtual_rooms.html b/AKOnline/templates/admin/AKOnline/status/event_virtual_rooms.html new file mode 100644 index 0000000000000000000000000000000000000000..a68526c308b2d4026eff5240945c58002133adac --- /dev/null +++ b/AKOnline/templates/admin/AKOnline/status/event_virtual_rooms.html @@ -0,0 +1,11 @@ +{% load i18n %} + +<ul> + {% for room in event.room_set.all %} + {% if room.virtual and room.virtual.url %} + <li> + <a href="{% url 'admin:AKOnline_virtualroom_change' room.pk %}">{{ room }} ({{ room.virtual.url | truncatechars:30 }})</a> + </li> + {% endif %} + {% endfor %} +</ul> diff --git a/AKOnline/views.py b/AKOnline/views.py index 44855eb41fa6f8562f6f65addcd7008e8fdfd0bd..13f089a893fd77e49f223eba9c4bd9d58f4242bb 100644 --- a/AKOnline/views.py +++ b/AKOnline/views.py @@ -2,7 +2,9 @@ from django.contrib import messages from django.http import HttpResponseRedirect from django.utils.translation import gettext_lazy as _ -from AKModel.views import AdminViewMixin, RoomCreationView +from AKModel.metaviews import status_manager +from AKModel.metaviews.status import TemplateStatusWidget +from AKModel.views.room import RoomCreationView from AKOnline.forms import RoomWithVirtualForm @@ -17,3 +19,10 @@ class RoomCreationWithVirtualView(RoomCreationView): if objects['virtual'] is not None: messages.success(self.request, _("Created related Virtual Room '%(vroom)s'" % {'vroom': objects['virtual']})) return HttpResponseRedirect(self.get_success_url()) + + +@status_manager.register(name="event_virtual_rooms") +class EventVirtualRoomsWidget(TemplateStatusWidget): + required_context_type = "event" + title = _("Virtual Rooms") + template_name = "admin/AKOnline/status/event_virtual_rooms.html" diff --git a/AKPlan/views.py b/AKPlan/views.py index 828e5e584e2646443e40f9dff623ca8b0a4d6391..513bca31838a5f33da37b3127a579cdc2a7c500a 100644 --- a/AKPlan/views.py +++ b/AKPlan/views.py @@ -7,7 +7,7 @@ from django.utils.datetime_safe import datetime from django.views.generic import ListView, DetailView from AKModel.models import AKSlot, Room, AKTrack -from AKModel.views import FilterByEventSlugMixin +from AKModel.metaviews.admin import FilterByEventSlugMixin class PlanIndexView(FilterByEventSlugMixin, ListView): diff --git a/AKScheduling/api.py b/AKScheduling/api.py index 98838e0f31c9b1232bdfb75e040ef9abbe3e1264..7155e6fe6e47081ccc6101699eba02bab78274ec 100644 --- a/AKScheduling/api.py +++ b/AKScheduling/api.py @@ -8,7 +8,7 @@ from rest_framework import viewsets, mixins, serializers, permissions from AKModel.availability.models import Availability from AKModel.models import Room, AKSlot, ConstraintViolation, DefaultSlot -from AKModel.views import EventSlugMixin +from AKModel.metaviews.admin import EventSlugMixin class ResourceSerializer(serializers.ModelSerializer): diff --git a/AKScheduling/views.py b/AKScheduling/views.py index 9d0302cfb1fa135a4dd3bf32f91c999cc2ceec13..7174287f22a37ad499fc602ec3e83c5ec687479f 100644 --- a/AKScheduling/views.py +++ b/AKScheduling/views.py @@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _ from django.views.generic import ListView, DetailView, UpdateView from AKModel.models import AKSlot, AKTrack, Event, AK, AKCategory -from AKModel.views import AdminViewMixin, FilterByEventSlugMixin, EventSlugMixin, IntermediateAdminView +from AKModel.metaviews.admin import EventSlugMixin, FilterByEventSlugMixin, AdminViewMixin, IntermediateAdminView from AKScheduling.forms import AKInterestForm from AKSubmission.forms import AKAddSlotForm diff --git a/AKSubmission/locale/de_DE/LC_MESSAGES/django.po b/AKSubmission/locale/de_DE/LC_MESSAGES/django.po index c0d16bb1165af858f3a31e79dcb3bbc8e49c7552..871d5d9c4e875e2d80df96af822b7df1f8536dd9 100644 --- a/AKSubmission/locale/de_DE/LC_MESSAGES/django.po +++ b/AKSubmission/locale/de_DE/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-01-01 20:05+0100\n" +"POT-Creation-Date: 2023-03-26 19:51+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" @@ -22,11 +22,11 @@ msgstr "" msgid "\"%(duration)s\" is not a valid duration" msgstr "\"%(duration)s\" ist keine gültige Dauer" -#: AKSubmission/forms.py:122 +#: AKSubmission/forms.py:117 msgid "Duration(s)" msgstr "Dauer(n)" -#: AKSubmission/forms.py:124 +#: AKSubmission/forms.py:119 msgid "" "Enter at least one planned duration (in hours). If your AK should have " "multiple slots, use multiple lines" @@ -34,28 +34,28 @@ msgstr "" "Mindestens eine geplante Dauer (in Stunden) angeben. Wenn der AK mehrere " "Slots haben soll, mehrere Zeilen verwenden" -#: AKSubmission/forms.py:179 +#: AKSubmission/forms.py:170 #: AKSubmission/templates/AKSubmission/ak_detail.html:309 msgid "Start" msgstr "Start" -#: AKSubmission/forms.py:180 +#: AKSubmission/forms.py:171 #: AKSubmission/templates/AKSubmission/ak_detail.html:310 msgid "End" msgstr "Ende" -#: AKSubmission/forms.py:181 +#: AKSubmission/forms.py:172 #: AKSubmission/templates/AKSubmission/ak_detail.html:239 #: AKSubmission/templates/AKSubmission/akslot_delete.html:35 msgid "Duration" msgstr "Dauer" -#: AKSubmission/forms.py:182 +#: AKSubmission/forms.py:173 #: AKSubmission/templates/AKSubmission/ak_detail.html:241 msgid "Room" msgstr "Raum" -#: AKSubmission/forms.py:186 +#: AKSubmission/forms.py:177 #: AKSubmission/templates/AKSubmission/ak_history.html:11 #: AKSubmission/templates/AKSubmission/akslot_delete.html:31 msgid "AK" @@ -73,8 +73,8 @@ msgstr "AK" #: AKSubmission/templates/AKSubmission/submission_not_configured.html:11 #: AKSubmission/templates/AKSubmission/submission_overview.html:7 #: AKSubmission/templates/AKSubmission/submission_overview.html:11 -#: AKSubmission/templates/AKSubmission/submission_overview.html:41 -#: AKSubmission/templates/AKSubmission/submit_new.html:34 +#: AKSubmission/templates/AKSubmission/submission_overview.html:36 +#: AKSubmission/templates/AKSubmission/submit_new.html:31 #: AKSubmission/templates/AKSubmission/submit_new_wish.html:13 msgid "AK Submission" msgstr "AK-Eintragung" @@ -242,7 +242,7 @@ msgstr "Mögliche Zeiten" #: AKSubmission/templates/AKSubmission/akslot_delete.html:7 #: AKSubmission/templates/AKSubmission/submission_not_configured.html:7 #: AKSubmission/templates/AKSubmission/submission_overview.html:7 -#: AKSubmission/templates/AKSubmission/submission_overview.html:45 +#: AKSubmission/templates/AKSubmission/submission_overview.html:40 #: AKSubmission/templates/AKSubmission/submit_new.html:9 #: AKSubmission/templates/AKSubmission/submit_new_wish.html:7 msgid "AKs" @@ -283,7 +283,7 @@ msgstr "Die Ergebnisse dieses AKs vorstellen" msgid "Intends to submit a resolution" msgstr "Beabsichtigt eine Resolution einzureichen" -#: AKSubmission/templates/AKSubmission/ak_list.html:6 AKSubmission/views.py:40 +#: AKSubmission/templates/AKSubmission/ak_list.html:6 AKSubmission/views.py:42 msgid "All AKs" msgstr "Alle AKs" @@ -307,22 +307,22 @@ msgstr "Details" msgid "There are no AKs in this category yet" msgstr "Es gibt noch keine AKs in dieser Kategorie" -#: AKSubmission/templates/AKSubmission/akmessage_add.html:28 +#: AKSubmission/templates/AKSubmission/akmessage_add.html:27 msgid "Send" msgstr "Senden" -#: AKSubmission/templates/AKSubmission/akmessage_add.html:32 -#: AKSubmission/templates/AKSubmission/akowner_create_update.html:27 -#: AKSubmission/templates/AKSubmission/akslot_add_update.html:30 -#: AKSubmission/templates/AKSubmission/submit_new.html:56 +#: AKSubmission/templates/AKSubmission/akmessage_add.html:31 +#: AKSubmission/templates/AKSubmission/akowner_create_update.html:26 +#: AKSubmission/templates/AKSubmission/akslot_add_update.html:29 +#: AKSubmission/templates/AKSubmission/submit_new.html:52 msgid "Reset Form" msgstr "Formular leeren" -#: AKSubmission/templates/AKSubmission/akmessage_add.html:36 -#: AKSubmission/templates/AKSubmission/akowner_create_update.html:31 -#: AKSubmission/templates/AKSubmission/akslot_add_update.html:34 -#: AKSubmission/templates/AKSubmission/akslot_delete.html:46 -#: AKSubmission/templates/AKSubmission/submit_new.html:60 +#: AKSubmission/templates/AKSubmission/akmessage_add.html:35 +#: AKSubmission/templates/AKSubmission/akowner_create_update.html:30 +#: AKSubmission/templates/AKSubmission/akslot_add_update.html:33 +#: AKSubmission/templates/AKSubmission/akslot_delete.html:45 +#: AKSubmission/templates/AKSubmission/submit_new.html:56 msgid "Cancel" msgstr "Abbrechen" @@ -332,8 +332,8 @@ msgstr "Abbrechen" msgid "AK Owner" msgstr "AK-Leitung" -#: AKSubmission/templates/AKSubmission/akowner_create_update.html:24 -#: AKSubmission/templates/AKSubmission/akslot_add_update.html:26 +#: AKSubmission/templates/AKSubmission/akowner_create_update.html:23 +#: AKSubmission/templates/AKSubmission/akslot_add_update.html:25 msgid "Continue" msgstr "Weiter" @@ -350,7 +350,7 @@ msgstr "AK-Dauer(n)" msgid "Do you really want to delete this AK Slot?" msgstr "Willst du diesen AK-Slot wirklich löschen?" -#: AKSubmission/templates/AKSubmission/akslot_delete.html:42 +#: AKSubmission/templates/AKSubmission/akslot_delete.html:41 msgid "Confirm" msgstr "Bestätigen" @@ -366,109 +366,117 @@ msgstr "" "Das System ist bisher nicht für Eintragen und Anzeige von AKs konfiguriert. " "Bitte versuche es später wieder." -#: AKSubmission/templates/AKSubmission/submission_overview.html:49 +#: AKSubmission/templates/AKSubmission/submission_overview.html:44 msgid "" "On this page you can see a list of current AKs, change them and add new ones." msgstr "" "Auf dieser Seite kannst du eine Liste von aktuellen AKs sehen, diese " "bearbeiten und neue hinzufügen." -#: AKSubmission/templates/AKSubmission/submission_overview.html:56 +#: AKSubmission/templates/AKSubmission/submission_overview.html:52 #: AKSubmission/templates/AKSubmission/submit_new_wish.html:7 #: AKSubmission/templates/AKSubmission/submit_new_wish.html:14 #: AKSubmission/templates/AKSubmission/submit_new_wish.html:18 msgid "New AK Wish" msgstr "Neuer AK-Wunsch" -#: AKSubmission/templates/AKSubmission/submission_overview.html:60 +#: AKSubmission/templates/AKSubmission/submission_overview.html:56 msgid "Who" msgstr "Wer" -#: AKSubmission/templates/AKSubmission/submission_overview.html:63 +#: AKSubmission/templates/AKSubmission/submission_overview.html:59 msgid "I do not own AKs yet" msgstr "Ich leite bisher keine AKs" -#: AKSubmission/templates/AKSubmission/submission_overview.html:71 +#: AKSubmission/templates/AKSubmission/submission_overview.html:67 #: AKSubmission/templates/AKSubmission/submit_new.html:9 -#: AKSubmission/templates/AKSubmission/submit_new.html:37 -#: AKSubmission/templates/AKSubmission/submit_new.html:44 +#: AKSubmission/templates/AKSubmission/submit_new.html:34 +#: AKSubmission/templates/AKSubmission/submit_new.html:41 msgid "New AK" msgstr "Neuer AK" -#: AKSubmission/templates/AKSubmission/submission_overview.html:77 +#: AKSubmission/templates/AKSubmission/submission_overview.html:73 msgid "Edit Person Info" msgstr "Personen-Info bearbeiten" -#: AKSubmission/templates/AKSubmission/submission_overview.html:84 +#: AKSubmission/templates/AKSubmission/submission_overview.html:81 msgid "This event is not active. You cannot add or change AKs" msgstr "" "Dieses Event is nicht aktiv. Es können keine AKs hinzugefügt oder bearbeitet " "werden" -#: AKSubmission/templates/AKSubmission/submit_new.html:52 +#: AKSubmission/templates/AKSubmission/submit_new.html:48 msgid "Submit" msgstr "Eintragen" -#: AKSubmission/views.py:71 +#: AKSubmission/views.py:73 msgid "Wishes" msgstr "Wünsche" -#: AKSubmission/views.py:71 +#: AKSubmission/views.py:73 msgid "AKs one would like to have" msgstr "" "AKs die sich gewünscht wurden, aber bei denen noch nicht klar ist, wer sie " "macht. Falls du dir das vorstellen kannst, trag dich einfach ein" -#: AKSubmission/views.py:91 +#: AKSubmission/views.py:93 msgid "Currently planned AKs" msgstr "Aktuell geplante AKs" -#: AKSubmission/views.py:181 +#: AKSubmission/views.py:186 msgid "Event inactive. Cannot create or update." msgstr "Event inaktiv. Hinzufügen/Bearbeiten nicht möglich." -#: AKSubmission/views.py:197 +#: AKSubmission/views.py:202 msgid "AK successfully created" msgstr "AK erfolgreich angelegt" -#: AKSubmission/views.py:247 +#: AKSubmission/views.py:252 msgid "AK successfully updated" msgstr "AK erfolgreich aktualisiert" -#: AKSubmission/views.py:285 +#: AKSubmission/views.py:290 #, python-brace-format msgid "Added '{owner}' as new owner of '{ak.name}'" msgstr "'{owner}' als neue Leitung von '{ak.name}' hinzugefügt" -#: AKSubmission/views.py:326 +#: AKSubmission/views.py:331 msgid "Person Info successfully updated" msgstr "Personen-Info erfolgreich aktualisiert" -#: AKSubmission/views.py:346 +#: AKSubmission/views.py:351 msgid "No user selected" msgstr "Keine Person ausgewählt" -#: AKSubmission/views.py:373 +#: AKSubmission/views.py:378 msgid "AK Slot successfully added" msgstr "AK-Slot erfolgreich angelegt" -#: AKSubmission/views.py:387 +#: AKSubmission/views.py:391 msgid "You cannot edit a slot that has already been scheduled" msgstr "Bereits geplante AK-Slots können nicht mehr bearbeitet werden" -#: AKSubmission/views.py:397 +#: AKSubmission/views.py:401 msgid "AK Slot successfully updated" msgstr "AK-Slot erfolgreich aktualisiert" -#: AKSubmission/views.py:410 +#: AKSubmission/views.py:413 msgid "You cannot delete a slot that has already been scheduled" msgstr "Bereits geplante AK-Slots können nicht mehr gelöscht werden" -#: AKSubmission/views.py:420 +#: AKSubmission/views.py:423 msgid "AK Slot successfully deleted" msgstr "AK-Slot erfolgreich angelegt" -#: AKSubmission/views.py:442 +#: AKSubmission/views.py:430 +msgid "Messages" +msgstr "Nachrichten" + +#: AKSubmission/views.py:440 +msgid "Delete all messages" +msgstr "Alle Nachrichten löschen" + +#: AKSubmission/views.py:463 msgid "Message to organizers successfully saved" msgstr "Nachricht an die Organisator*innen erfolgreich gespeichert" diff --git a/AKSubmission/views.py b/AKSubmission/views.py index 1b865843f6b452c709924d3dd9765fdee554655d..d62762e1e3676f23b80a6ad01a8bc4dfa7079a23 100644 --- a/AKSubmission/views.py +++ b/AKSubmission/views.py @@ -13,9 +13,10 @@ from django.views import View from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView, TemplateView from AKModel.availability.models import Availability +from AKModel.metaviews import status_manager +from AKModel.metaviews.status import TemplateStatusWidget from AKModel.models import AK, AKCategory, AKOwner, AKSlot, AKTrack, AKOrgaMessage -from AKModel.views import EventSlugMixin -from AKModel.views import FilterByEventSlugMixin +from AKModel.metaviews.admin import EventSlugMixin, FilterByEventSlugMixin from AKSubmission.api import ak_interest_indication_active from AKSubmission.forms import AKWishForm, AKOwnerForm, AKSubmissionForm, AKDurationForm, AKOrgaMessageForm, \ AKForm @@ -423,6 +424,25 @@ class AKSlotDeleteView(EventSlugMixin, EventInactiveRedirectMixin, DeleteView): return self.object.ak.detail_url +@status_manager.register(name="event_ak_messages") +class EventAKMessagesWidget(TemplateStatusWidget): + required_context_type = "event" + title = _("Messages") + template_name = "admin/AKModel/render_ak_messages.html" + + def get_context_data(self, context) -> dict: + context["ak_messages"] = AKOrgaMessage.objects.filter(ak__event=context["event"]) + return context + + def render_actions(self, context: {}) -> list[dict]: + return [ + { + "text": _("Delete all messages"), + "url": reverse_lazy("admin:ak_delete_orga_messages", kwargs={"event_slug": context["event"].slug}), + }, + ] + + class AKAddOrgaMessageView(EventSlugMixin, CreateView): model = AKOrgaMessage form_class = AKOrgaMessageForm