diff --git a/.docker/extra_requirements.txt b/.docker/extra_requirements.txt index 55e14c229a0c7e179c89afe7ab63e9b574fbc70b..f8498f044fad1c233232723f0c40bca1a82bc536 100644 --- a/.docker/extra_requirements.txt +++ b/.docker/extra_requirements.txt @@ -1 +1 @@ -uwsgi==2.0.25.1 +uwsgi==2.0.28 diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5855c87c8f33c1695f0fb005197a1fba63f1179b..4cde1bfb74cba5daf1c6a4b734edce8ad68a5cd6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: python:3.10 +image: python:3.11 services: - mysql @@ -26,10 +26,9 @@ cache: - pip install pylint-gitlab pylint-django - mysql --version -check: +migrations: extends: .before_script_template script: - - ./Utils/check.sh --all - source venv/bin/activate - ./manage.py makemigrations --dry-run --check diff --git a/AKDashboard/tests.py b/AKDashboard/tests.py index 65b59fd9cdea19830b87b2577f53886619277a27..59328adf517d22a1793df63f67782254b0958f94 100644 --- a/AKDashboard/tests.py +++ b/AKDashboard/tests.py @@ -1,11 +1,12 @@ import zoneinfo + from django.apps import apps -from django.test import TestCase, override_settings +from django.test import override_settings, TestCase from django.urls import reverse from django.utils.timezone import now from AKDashboard.models import DashboardButton -from AKModel.models import Event, AK, AKCategory +from AKModel.models import AK, AKCategory, Event from AKModel.tests.test_views import BasicViewTests @@ -13,6 +14,7 @@ class DashboardTests(TestCase): """ Specific Dashboard Tests """ + @classmethod def setUpTestData(cls): """ @@ -20,17 +22,17 @@ class DashboardTests(TestCase): """ super().setUpTestData() cls.event = Event.objects.create( - name="Dashboard Test Event", - slug="dashboardtest", - timezone=zoneinfo.ZoneInfo("Europe/Berlin"), - start=now(), - end=now(), - active=True, - plan_hidden=False, + name="Dashboard Test Event", + slug="dashboardtest", + timezone=zoneinfo.ZoneInfo("Europe/Berlin"), + start=now(), + end=now(), + active=True, + plan_hidden=False, ) cls.default_category = AKCategory.objects.create( - name="Test Category", - event=cls.event, + name="Test Category", + event=cls.event, ) def test_dashboard_view(self): @@ -62,12 +64,12 @@ class DashboardTests(TestCase): # History should be empty response = self.client.get(url) - self.assertQuerysetEqual(response.context["recent_changes"], []) + self.assertQuerySetEqual(response.context["recent_changes"], []) AK.objects.create( - name="Test AK", - category=self.default_category, - event=self.event, + name="Test AK", + category=self.default_category, + event=self.event, ) # History should now contain one AK (Test AK) @@ -154,8 +156,8 @@ class DashboardTests(TestCase): self.assertNotContains(response, "Dashboard Button Test") DashboardButton.objects.create( - text="Dashboard Button Test", - event=self.event + text="Dashboard Button Test", + event=self.event ) response = self.client.get(url_event_dashboard) diff --git a/AKModel/locale/de_DE/LC_MESSAGES/django.po b/AKModel/locale/de_DE/LC_MESSAGES/django.po index 38b3d54a9101367a201b23ac589ff61f217d7775..00409411289c42f855f3f8a048a4175d87424800 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: 2025-03-03 00:42+0000\n" +"POT-Creation-Date: 2025-03-04 17:35+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -33,8 +33,8 @@ msgstr "Plan veröffentlichen" msgid "Unpublish plan" msgstr "Plan verbergen" -#: AKModel/admin.py:170 AKModel/models.py:875 AKModel/models.py:1329 -#: AKModel/models.py:1365 AKModel/templates/admin/AKModel/aks_by_user.html:12 +#: AKModel/admin.py:170 AKModel/models.py:892 AKModel/models.py:1349 +#: AKModel/models.py:1385 AKModel/templates/admin/AKModel/aks_by_user.html:12 #: AKModel/templates/admin/AKModel/status/event_aks.html:10 #: AKModel/views/manage.py:75 AKModel/views/status.py:102 msgid "AKs" @@ -60,11 +60,11 @@ msgstr "In Wiki-Syntax exportieren" 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:335 AKModel/views/ak.py:123 +#: AKModel/admin.py:335 AKModel/views/ak.py:120 msgid "Reset interest in AKs" msgstr "Interesse an AKs zurücksetzen" -#: AKModel/admin.py:345 AKModel/views/ak.py:138 +#: AKModel/admin.py:345 AKModel/views/ak.py:135 msgid "Reset AKs' interest counters" msgstr "Interessenszähler der AKs zurücksetzen" @@ -125,19 +125,19 @@ 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:43 AKModel/models.py:166 -#: AKModel/models.py:654 AKModel/models.py:731 AKModel/models.py:764 -#: AKModel/models.py:790 AKModel/models.py:809 AKModel/models.py:865 -#: AKModel/models.py:1023 AKModel/models.py:1100 AKModel/models.py:1268 -#: AKModel/models.py:1325 AKModel/models.py:1516 +#: AKModel/availability/models.py:43 AKModel/models.py:179 +#: AKModel/models.py:669 AKModel/models.py:746 AKModel/models.py:779 +#: AKModel/models.py:805 AKModel/models.py:824 AKModel/models.py:882 +#: AKModel/models.py:1041 AKModel/models.py:1118 AKModel/models.py:1287 +#: AKModel/models.py:1345 AKModel/models.py:1537 msgid "Event" msgstr "Event" -#: AKModel/availability/models.py:44 AKModel/models.py:655 -#: AKModel/models.py:732 AKModel/models.py:765 AKModel/models.py:791 -#: AKModel/models.py:810 AKModel/models.py:866 AKModel/models.py:1024 -#: AKModel/models.py:1101 AKModel/models.py:1269 AKModel/models.py:1326 -#: AKModel/models.py:1517 +#: AKModel/availability/models.py:44 AKModel/models.py:670 +#: AKModel/models.py:747 AKModel/models.py:780 AKModel/models.py:806 +#: AKModel/models.py:825 AKModel/models.py:883 AKModel/models.py:1042 +#: AKModel/models.py:1119 AKModel/models.py:1288 AKModel/models.py:1346 +#: AKModel/models.py:1538 msgid "Associated event" msgstr "Zugehöriges Event" @@ -149,8 +149,8 @@ msgstr "Person" msgid "Person whose availability this is" msgstr "Person deren Verfügbarkeit hier abgebildet wird" -#: AKModel/availability/models.py:61 AKModel/models.py:1027 -#: AKModel/models.py:1090 AKModel/models.py:1335 +#: AKModel/availability/models.py:61 AKModel/models.py:1045 +#: AKModel/models.py:1108 AKModel/models.py:1355 msgid "Room" msgstr "Raum" @@ -158,8 +158,8 @@ msgstr "Raum" msgid "Room whose availability this is" msgstr "Raum dessen Verfügbarkeit hier abgebildet wird" -#: AKModel/availability/models.py:70 AKModel/models.py:874 -#: AKModel/models.py:1089 AKModel/models.py:1263 +#: AKModel/availability/models.py:70 AKModel/models.py:891 +#: AKModel/models.py:1107 AKModel/models.py:1282 msgid "AK" msgstr "AK" @@ -167,8 +167,8 @@ msgstr "AK" msgid "AK whose availability this is" msgstr "Verfügbarkeiten" -#: AKModel/availability/models.py:79 AKModel/models.py:735 -#: AKModel/models.py:1341 +#: AKModel/availability/models.py:79 AKModel/models.py:750 +#: AKModel/models.py:1361 msgid "AK Category" msgstr "AK-Kategorie" @@ -176,7 +176,7 @@ msgstr "AK-Kategorie" msgid "AK Category whose availability this is" msgstr "AK-Kategorie, deren Verfügbarkeit hier abgebildet wird" -#: AKModel/availability/models.py:309 AKModel/models.py:923 +#: AKModel/availability/models.py:309 AKModel/models.py:940 msgid "Availabilities" msgstr "Verfügbarkeiten" @@ -242,7 +242,7 @@ msgstr "" "fürWünsche markieren, z.B. um während der Präsentation auf einem Touchscreen " "ausgefüllt zu werden?" -#: AKModel/forms.py:198 AKModel/models.py:1510 +#: AKModel/forms.py:198 AKModel/models.py:1531 msgid "Default Slots" msgstr "Standardslots" @@ -289,7 +289,7 @@ msgstr "JSON-Daten" msgid "JSON data from the scheduling solver" msgstr "JSON-Daten, die der scheduling-solver produziert hat" -#: AKModel/metaviews/admin.py:156 AKModel/models.py:129 +#: AKModel/metaviews/admin.py:156 AKModel/models.py:141 msgid "Start" msgstr "Start" @@ -314,67 +314,75 @@ msgstr "Aktivieren?" msgid "Finish" msgstr "Abschluss" -#: AKModel/models.py:120 AKModel/models.py:723 AKModel/models.py:761 -#: AKModel/models.py:788 AKModel/models.py:807 AKModel/models.py:825 -#: AKModel/models.py:1015 +#: AKModel/models.py:26 +msgid "May not contain quotation marks" +msgstr "Darf keine Anführungszeichen enthalten" + +#: AKModel/models.py:29 +msgid "Must contain at least one letter or digit" +msgstr "Muss mindestens einen Buchstaben oder eine Ziffer enthalten" + +#: AKModel/models.py:132 AKModel/models.py:738 AKModel/models.py:776 +#: AKModel/models.py:803 AKModel/models.py:822 AKModel/models.py:840 +#: AKModel/models.py:1033 msgid "Name" msgstr "Name" -#: AKModel/models.py:121 +#: AKModel/models.py:133 msgid "Name or iteration of the event" msgstr "Name oder Iteration des Events" -#: AKModel/models.py:122 +#: AKModel/models.py:134 msgid "Short Form" msgstr "Kurzer Name" -#: AKModel/models.py:123 +#: AKModel/models.py:135 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:125 +#: AKModel/models.py:137 msgid "Place" msgstr "Ort" -#: AKModel/models.py:126 +#: AKModel/models.py:138 msgid "City etc. the event takes place in" msgstr "Stadt o.ä. in der das Event stattfindet" -#: AKModel/models.py:128 +#: AKModel/models.py:140 msgid "Time Zone" msgstr "Zeitzone" -#: AKModel/models.py:128 +#: AKModel/models.py:140 msgid "Time Zone where this event takes place in" msgstr "Zeitzone in der das Event stattfindet" -#: AKModel/models.py:129 +#: AKModel/models.py:141 msgid "Time the event begins" msgstr "Zeit zu der das Event beginnt" -#: AKModel/models.py:130 +#: AKModel/models.py:142 msgid "End" msgstr "Ende" -#: AKModel/models.py:130 +#: AKModel/models.py:142 msgid "Time the event ends" msgstr "Zeit zu der das Event endet" -#: AKModel/models.py:131 +#: AKModel/models.py:143 msgid "Resolution Deadline" msgstr "Resolutionsdeadline" -#: AKModel/models.py:132 +#: AKModel/models.py:144 msgid "When should AKs with intention to submit a resolution be done?" msgstr "Wann sollen AKs mit Resolutionsabsicht stattgefunden haben?" -#: AKModel/models.py:134 +#: AKModel/models.py:146 msgid "Interest Window Start" msgstr "Beginn Interessensbekundung" -#: AKModel/models.py:136 +#: AKModel/models.py:148 msgid "" "Opening time for expression of interest. When left blank, no interest " "indication will be possible." @@ -382,71 +390,71 @@ msgstr "" "Öffnungszeitpunkt für die Angabe von Interesse an AKs.Wenn das Feld leer " "bleibt, wird keine Abgabe von Interesse möglich sein." -#: AKModel/models.py:138 +#: AKModel/models.py:151 msgid "Interest Window End" msgstr "Ende Interessensbekundung" -#: AKModel/models.py:139 +#: AKModel/models.py:152 msgid "Closing time for expression of interest." msgstr "Öffnungszeitpunkt für die Angabe von Interesse an AKs." -#: AKModel/models.py:141 +#: AKModel/models.py:154 msgid "Public event" msgstr "Öffentliches Event" -#: AKModel/models.py:142 +#: AKModel/models.py:155 msgid "Show this event on overview page." msgstr "Zeige dieses Event auf der Übersichtseite an" -#: AKModel/models.py:144 +#: AKModel/models.py:157 msgid "Active State" msgstr "Aktiver Status" -#: AKModel/models.py:144 +#: AKModel/models.py:157 msgid "Marks currently active events" msgstr "Markiert aktuell aktive Events" -#: AKModel/models.py:145 +#: AKModel/models.py:158 msgid "Plan Hidden" msgstr "Plan verborgen" -#: AKModel/models.py:145 +#: AKModel/models.py:158 msgid "Hides plan for non-staff users" msgstr "Verbirgt den Plan für Nutzer*innen ohne erweiterte Rechte" -#: AKModel/models.py:147 +#: AKModel/models.py:160 msgid "Plan published at" msgstr "Plan veröffentlicht am/um" -#: AKModel/models.py:148 +#: AKModel/models.py:161 msgid "Timestamp at which the plan was published" msgstr "Zeitpunkt, zu dem der Plan veröffentlicht wurde" -#: AKModel/models.py:150 +#: AKModel/models.py:163 msgid "Base URL" msgstr "URL-Prefix" -#: AKModel/models.py:150 +#: AKModel/models.py:163 msgid "Prefix for wiki link construction" msgstr "Prefix für die automatische Generierung von Wiki-Links" -#: AKModel/models.py:151 +#: AKModel/models.py:164 msgid "Wiki Export Template Name" msgstr "Wiki-Export Templatename" -#: AKModel/models.py:152 +#: AKModel/models.py:165 msgid "Default Slot Length" msgstr "Standardslotlänge" -#: AKModel/models.py:153 +#: AKModel/models.py:166 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:154 +#: AKModel/models.py:167 msgid "Export Slot Length" msgstr "Export-Slotlänge" -#: AKModel/models.py:156 +#: AKModel/models.py:169 msgid "" "Slot duration in hours that is used in the timeslot discretization, when " "this event is exported for the solver." @@ -454,11 +462,11 @@ msgstr "" "Länge von Slots (in Stunden) in der Zeitslot-Diskretisierung beim JSON-" "Export dieses Events." -#: AKModel/models.py:161 +#: AKModel/models.py:174 msgid "Contact email address" msgstr "E-Mail Kontaktadresse" -#: AKModel/models.py:162 +#: AKModel/models.py:175 msgid "" "An email address that is displayed on every page and can be used for all " "kinds of questions" @@ -466,16 +474,16 @@ msgstr "" "Eine Mailadresse die auf jeder Seite angezeigt wird und für alle Arten von " "Fragen genutzt werden kann" -#: AKModel/models.py:167 +#: AKModel/models.py:180 msgid "Events" msgstr "Events" -#: AKModel/models.py:444 +#: AKModel/models.py:457 #, python-brace-format msgid "AK {ak_name} is not assigned any timeslot by the solver" msgstr "Dem AK {ak_name} wurde vom Solver kein Zeitslot zugewiesen" -#: AKModel/models.py:454 +#: AKModel/models.py:467 #, python-brace-format msgid "" "Duration of AK {ak_name} assigned by solver ({solver_duration} hours) is " @@ -485,7 +493,7 @@ msgstr "" "Stunden) ist kürzer als die aktuell vorgesehene Dauer des Slots " "({slot_duration} Stunden)" -#: AKModel/models.py:468 +#: AKModel/models.py:481 #, python-brace-format msgid "" "Fixed AK {ak_name} assigned by solver to room {solver_room} is fixed to room " @@ -494,7 +502,7 @@ msgstr "" "Dem fix geplanten AK {ak_name} wurde vom Solver Raum {solver_room} " "zugewiesen, dabei ist der AK bereits fix in Raum {slot_room} eingeplant." -#: AKModel/models.py:479 +#: AKModel/models.py:492 #, python-brace-format msgid "" "Fixed AK {ak_name} assigned by solver to start at {solver_start} is fixed to " @@ -503,71 +511,71 @@ msgstr "" "Dem fix geplanten AK {ak_name} wurde vom Solver die Startzeit {solver_start} " "zugewiesen, dabei ist der AK bereits für {slot_start} eingeplant." -#: AKModel/models.py:649 +#: AKModel/models.py:662 msgid "Nickname" msgstr "Spitzname" -#: AKModel/models.py:649 +#: AKModel/models.py:664 msgid "Name to identify an AK owner by" msgstr "Name, durch den eine AK-Leitung identifiziert wird" -#: AKModel/models.py:650 +#: AKModel/models.py:665 msgid "Slug" msgstr "Slug" -#: AKModel/models.py:650 +#: AKModel/models.py:665 msgid "Slug for URL generation" msgstr "Slug für URL-Generierung" -#: AKModel/models.py:651 +#: AKModel/models.py:666 msgid "Institution" msgstr "Instutution" -#: AKModel/models.py:651 +#: AKModel/models.py:666 msgid "Uni etc." msgstr "Universität o.ä." -#: AKModel/models.py:652 AKModel/models.py:834 +#: AKModel/models.py:667 AKModel/models.py:851 msgid "Web Link" msgstr "Internet Link" -#: AKModel/models.py:652 +#: AKModel/models.py:667 msgid "Link to Homepage" msgstr "Link zu Homepage oder Webseite" -#: AKModel/models.py:658 AKModel/models.py:1334 +#: AKModel/models.py:673 AKModel/models.py:1354 msgid "AK Owner" msgstr "AK-Leitung" -#: AKModel/models.py:659 +#: AKModel/models.py:674 msgid "AK Owners" msgstr "AK-Leitungen" -#: AKModel/models.py:723 +#: AKModel/models.py:738 msgid "Name of the AK Category" msgstr "Name der AK-Kategorie" -#: AKModel/models.py:724 AKModel/models.py:762 +#: AKModel/models.py:739 AKModel/models.py:777 msgid "Color" msgstr "Farbe" -#: AKModel/models.py:724 AKModel/models.py:762 +#: AKModel/models.py:739 AKModel/models.py:777 msgid "Color for displaying" msgstr "Farbe für die Anzeige" -#: AKModel/models.py:725 AKModel/models.py:828 +#: AKModel/models.py:740 AKModel/models.py:845 msgid "Description" msgstr "Beschreibung" -#: AKModel/models.py:726 +#: AKModel/models.py:741 msgid "Short description of this AK Category" msgstr "Beschreibung der AK-Kategorie" -#: AKModel/models.py:727 +#: AKModel/models.py:742 msgid "Present by default" msgstr "Defaultmäßig präsentieren" -#: AKModel/models.py:728 +#: AKModel/models.py:743 msgid "" "Present AKs of this category by default if AK owner did not specify whether " "this AK should be presented?" @@ -575,152 +583,152 @@ msgstr "" "AKs dieser Kategorie standardmäßig vorstellen, wenn die Leitungen das für " "ihren AK nicht explizit spezifiziert haben?" -#: AKModel/models.py:736 +#: AKModel/models.py:751 msgid "AK Categories" msgstr "AK-Kategorien" -#: AKModel/models.py:761 +#: AKModel/models.py:776 msgid "Name of the AK Track" msgstr "Name des AK-Tracks" -#: AKModel/models.py:768 +#: AKModel/models.py:783 msgid "AK Track" msgstr "AK-Track" -#: AKModel/models.py:769 +#: AKModel/models.py:784 msgid "AK Tracks" msgstr "AK-Tracks" -#: AKModel/models.py:788 +#: AKModel/models.py:803 msgid "Name of the Requirement" msgstr "Name der Anforderung" -#: AKModel/models.py:794 AKModel/models.py:1338 +#: AKModel/models.py:809 AKModel/models.py:1358 msgid "AK Requirement" msgstr "AK-Anforderung" -#: AKModel/models.py:795 +#: AKModel/models.py:810 msgid "AK Requirements" msgstr "AK-Anforderungen" -#: AKModel/models.py:807 +#: AKModel/models.py:822 msgid "Name describing the type" msgstr "Name, der den Typ beschreibt" -#: AKModel/models.py:813 +#: AKModel/models.py:828 msgid "AK Type" msgstr "AK Typ" -#: AKModel/models.py:814 +#: AKModel/models.py:829 msgid "AK Types" msgstr "AK-Typen" -#: AKModel/models.py:825 +#: AKModel/models.py:840 msgid "Name of the AK" msgstr "Name des AKs" -#: AKModel/models.py:826 +#: AKModel/models.py:842 msgid "Short Name" msgstr "Kurzer Name" -#: AKModel/models.py:827 +#: AKModel/models.py:844 msgid "Name displayed in the schedule" msgstr "Name zur Anzeige im AK-Plan" -#: AKModel/models.py:828 +#: AKModel/models.py:845 msgid "Description of the AK" msgstr "Beschreibung des AKs" -#: AKModel/models.py:830 +#: AKModel/models.py:847 msgid "Owners" msgstr "Leitungen" -#: AKModel/models.py:831 +#: AKModel/models.py:848 msgid "Those organizing the AK" msgstr "Menschen, die den AK organisieren und halten" -#: AKModel/models.py:834 +#: AKModel/models.py:851 msgid "Link to wiki page" msgstr "Link zur Wiki Seite" -#: AKModel/models.py:835 +#: AKModel/models.py:852 msgid "Protocol Link" msgstr "Protokolllink" -#: AKModel/models.py:835 +#: AKModel/models.py:852 msgid "Link to protocol" msgstr "Link zum Protokoll" -#: AKModel/models.py:837 +#: AKModel/models.py:854 msgid "Category" msgstr "Kategorie" -#: AKModel/models.py:838 +#: AKModel/models.py:855 msgid "Category of the AK" msgstr "Kategorie des AKs" -#: AKModel/models.py:839 AKModel/models.py:904 +#: AKModel/models.py:856 AKModel/models.py:921 msgid "Types" msgstr "Typen" -#: AKModel/models.py:840 +#: AKModel/models.py:857 msgid "This AK is" msgstr "Dieser AK ist" -#: AKModel/models.py:841 +#: AKModel/models.py:858 msgid "Track" msgstr "Track" -#: AKModel/models.py:842 +#: AKModel/models.py:859 msgid "Track the AK belongs to" msgstr "Track zu dem der AK gehört" -#: AKModel/models.py:844 +#: AKModel/models.py:861 msgid "Resolution Intention" msgstr "Resolutionsabsicht" -#: AKModel/models.py:845 +#: AKModel/models.py:862 msgid "Intends to submit a resolution" msgstr "Beabsichtigt eine Resolution einzureichen" -#: AKModel/models.py:846 +#: AKModel/models.py:863 msgid "Present this AK" msgstr "AK präsentieren" -#: AKModel/models.py:847 +#: AKModel/models.py:864 msgid "Present results of this AK" msgstr "Die Ergebnisse dieses AKs vorstellen" -#: AKModel/models.py:849 AKModel/models.py:902 AKModel/views/status.py:175 +#: AKModel/models.py:866 AKModel/models.py:919 AKModel/views/status.py:175 msgid "Requirements" msgstr "Anforderungen" -#: AKModel/models.py:850 +#: AKModel/models.py:867 msgid "AK's Requirements" msgstr "Anforderungen des AKs" -#: AKModel/models.py:852 +#: AKModel/models.py:869 msgid "Conflicting AKs" msgstr "AK-Konflikte" -#: AKModel/models.py:853 +#: AKModel/models.py:870 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:854 +#: AKModel/models.py:871 msgid "Prerequisite AKs" msgstr "Vorausgesetzte AKs" -#: AKModel/models.py:855 +#: AKModel/models.py:872 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:857 +#: AKModel/models.py:874 msgid "Organizational Notes" msgstr "Notizen zur Organisation" -#: AKModel/models.py:858 +#: AKModel/models.py:875 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/" @@ -730,299 +738,299 @@ msgstr "" "Anmerkungen bitte den Button für Direktnachrichten verwenden (nach dem " "Anlegen/Bearbeiten)." -#: AKModel/models.py:861 AKModel/models.py:900 +#: AKModel/models.py:878 AKModel/models.py:917 msgid "Interest" msgstr "Interesse" -#: AKModel/models.py:861 +#: AKModel/models.py:878 msgid "Expected number of people" msgstr "Erwartete Personenzahl" -#: AKModel/models.py:862 +#: AKModel/models.py:879 msgid "Interest Counter" msgstr "Interessenszähler" -#: AKModel/models.py:863 +#: AKModel/models.py:880 msgid "People who have indicated interest online" msgstr "Anzahl Personen, die online Interesse bekundet haben" -#: AKModel/models.py:868 +#: AKModel/models.py:885 msgid "Export?" msgstr "Export?" -#: AKModel/models.py:869 +#: AKModel/models.py:886 msgid "Include AK in wiki export?" msgstr "AK bei Wiki-Export berücksichtigen?" -#: AKModel/models.py:919 +#: AKModel/models.py:936 msgid "Conflicts" msgstr "Konflikte" -#: AKModel/models.py:922 +#: AKModel/models.py:939 msgid "Prerequisites" msgstr "Voraussetzungen" -#: AKModel/models.py:1015 +#: AKModel/models.py:1033 msgid "Name or number of the room" msgstr "Name oder Nummer des Raums" -#: AKModel/models.py:1016 +#: AKModel/models.py:1034 msgid "Location" msgstr "Ort" -#: AKModel/models.py:1017 +#: AKModel/models.py:1035 msgid "Name or number of the location" msgstr "Name oder Nummer des Ortes" -#: AKModel/models.py:1018 +#: AKModel/models.py:1036 msgid "Capacity" msgstr "Kapazität" -#: AKModel/models.py:1019 +#: AKModel/models.py:1037 msgid "Maximum number of people (-1 for unlimited)." msgstr "Maximale Personenzahl (-1 wenn unbeschränkt)." -#: AKModel/models.py:1020 +#: AKModel/models.py:1038 msgid "Properties" msgstr "Eigenschaften" -#: AKModel/models.py:1021 +#: AKModel/models.py:1039 msgid "AK requirements fulfilled by the room" msgstr "AK-Anforderungen, die dieser Raum erfüllt" -#: AKModel/models.py:1028 AKModel/views/status.py:59 +#: AKModel/models.py:1046 AKModel/views/status.py:59 msgid "Rooms" msgstr "Räume" -#: AKModel/models.py:1089 +#: AKModel/models.py:1107 msgid "AK being mapped" msgstr "AK, der zugeordnet wird" -#: AKModel/models.py:1091 +#: AKModel/models.py:1109 msgid "Room the AK will take place in" msgstr "Raum in dem der AK stattfindet" -#: AKModel/models.py:1092 AKModel/models.py:1513 +#: AKModel/models.py:1110 AKModel/models.py:1534 msgid "Slot Begin" msgstr "Beginn des Slots" -#: AKModel/models.py:1092 AKModel/models.py:1513 +#: AKModel/models.py:1110 AKModel/models.py:1534 msgid "Time and date the slot begins" msgstr "Zeit und Datum zu der der AK beginnt" -#: AKModel/models.py:1094 +#: AKModel/models.py:1112 msgid "Duration" msgstr "Dauer" -#: AKModel/models.py:1095 +#: AKModel/models.py:1113 msgid "Length in hours" msgstr "Länge in Stunden" -#: AKModel/models.py:1097 +#: AKModel/models.py:1115 msgid "Scheduling fixed" msgstr "Planung fix" -#: AKModel/models.py:1098 +#: AKModel/models.py:1116 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:1103 +#: AKModel/models.py:1121 msgid "Last update" msgstr "Letzte Aktualisierung" -#: AKModel/models.py:1106 +#: AKModel/models.py:1124 msgid "AK Slot" msgstr "AK-Slot" -#: AKModel/models.py:1107 AKModel/models.py:1331 AKModel/models.py:1366 +#: AKModel/models.py:1125 AKModel/models.py:1351 AKModel/models.py:1386 msgid "AK Slots" msgstr "AK-Slot" -#: AKModel/models.py:1129 AKModel/models.py:1138 +#: AKModel/models.py:1147 AKModel/models.py:1156 msgid "Not scheduled yet" msgstr "Noch nicht geplant" -#: AKModel/models.py:1264 +#: AKModel/models.py:1283 msgid "AK this message belongs to" msgstr "AK zu dem die Nachricht gehört" -#: AKModel/models.py:1265 +#: AKModel/models.py:1284 msgid "Message text" msgstr "Nachrichtentext" -#: AKModel/models.py:1266 +#: AKModel/models.py:1285 msgid "Message to the organizers. This is not publicly visible." msgstr "" "Nachricht an die Organisator*innen. Diese ist nicht öffentlich sichtbar." -#: AKModel/models.py:1270 +#: AKModel/models.py:1289 msgid "Resolved" msgstr "Erledigt" -#: AKModel/models.py:1271 +#: AKModel/models.py:1290 msgid "This message has been resolved (no further action needed)" msgstr "" "Diese Nachricht wurde vollständig bearbeitet (keine weiteren Aktionen " "notwendig)" -#: AKModel/models.py:1274 +#: AKModel/models.py:1293 msgid "AK Orga Message" msgstr "AK-Organachricht" -#: AKModel/models.py:1275 +#: AKModel/models.py:1294 msgid "AK Orga Messages" msgstr "AK-Organachrichten" -#: AKModel/models.py:1292 +#: AKModel/models.py:1312 msgid "Constraint Violation" msgstr "Constraintverletzung" -#: AKModel/models.py:1293 +#: AKModel/models.py:1313 msgid "Constraint Violations" msgstr "Constraintverletzungen" -#: AKModel/models.py:1300 +#: AKModel/models.py:1320 msgid "Owner has two parallel slots" msgstr "Leitung hat zwei Slots parallel" -#: AKModel/models.py:1301 +#: AKModel/models.py:1321 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:1302 +#: AKModel/models.py:1322 msgid "Room has two AK slots scheduled at the same time" msgstr "Raum hat zwei AK Slots gleichzeitig" -#: AKModel/models.py:1303 +#: AKModel/models.py:1323 msgid "Room does not satisfy the requirement of the scheduled AK" msgstr "Room erfüllt die Anforderungen des platzierten AKs nicht" -#: AKModel/models.py:1304 +#: AKModel/models.py:1324 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:1305 +#: AKModel/models.py:1325 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:1307 +#: AKModel/models.py:1327 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:1308 +#: AKModel/models.py:1328 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:1309 +#: AKModel/models.py:1329 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:1310 +#: AKModel/models.py:1330 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:1311 +#: AKModel/models.py:1331 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:1317 +#: AKModel/models.py:1337 msgid "Warning" msgstr "Warnung" -#: AKModel/models.py:1318 +#: AKModel/models.py:1338 msgid "Violation" msgstr "Verletzung" -#: AKModel/models.py:1320 +#: AKModel/models.py:1340 msgid "Type" msgstr "Art" -#: AKModel/models.py:1321 +#: AKModel/models.py:1341 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:1322 +#: AKModel/models.py:1342 msgid "Level" msgstr "Level" -#: AKModel/models.py:1323 +#: AKModel/models.py:1343 msgid "Severity level of the violation" msgstr "Schweregrad der Verletzung" -#: AKModel/models.py:1330 +#: AKModel/models.py:1350 msgid "AK(s) belonging to this constraint" msgstr "AK(s), die zu diesem Constraint gehören" -#: AKModel/models.py:1332 +#: AKModel/models.py:1352 msgid "AK Slot(s) belonging to this constraint" msgstr "AK Slot(s), die zu diesem Constraint gehören" -#: AKModel/models.py:1334 +#: AKModel/models.py:1354 msgid "AK Owner belonging to this constraint" msgstr "AK Leitung(en), die zu diesem Constraint gehören" -#: AKModel/models.py:1336 +#: AKModel/models.py:1356 msgid "Room belonging to this constraint" msgstr "Raum, der zu diesem Constraint gehört" -#: AKModel/models.py:1339 +#: AKModel/models.py:1359 msgid "AK Requirement belonging to this constraint" msgstr "AK Anforderung, die zu diesem Constraint gehört" -#: AKModel/models.py:1341 +#: AKModel/models.py:1361 msgid "AK Category belonging to this constraint" msgstr "AK Kategorie, di zu diesem Constraint gehört" -#: AKModel/models.py:1343 +#: AKModel/models.py:1363 msgid "Comment" msgstr "Kommentar" -#: AKModel/models.py:1343 +#: AKModel/models.py:1363 msgid "Comment or further details for this violation" msgstr "Kommentar oder weitere Details zu dieser Vereletzung" -#: AKModel/models.py:1346 +#: AKModel/models.py:1366 msgid "Timestamp" msgstr "Timestamp" -#: AKModel/models.py:1346 +#: AKModel/models.py:1366 msgid "Time of creation" msgstr "Zeitpunkt der ERstellung" -#: AKModel/models.py:1347 +#: AKModel/models.py:1367 msgid "Manually Resolved" msgstr "Manuell behoben" -#: AKModel/models.py:1348 +#: AKModel/models.py:1368 msgid "Mark this violation manually as resolved" msgstr "Markiere diese Verletzung manuell als behoben" -#: AKModel/models.py:1375 AKModel/templates/admin/AKModel/aks_by_user.html:22 +#: AKModel/models.py:1395 AKModel/templates/admin/AKModel/aks_by_user.html:22 #: AKModel/templates/admin/AKModel/requirements_overview.html:27 msgid "Details" msgstr "Details" -#: AKModel/models.py:1509 +#: AKModel/models.py:1530 msgid "Default Slot" msgstr "Standardslot" -#: AKModel/models.py:1514 +#: AKModel/models.py:1535 msgid "Slot End" msgstr "Ende des Slots" -#: AKModel/models.py:1514 +#: AKModel/models.py:1535 msgid "Time and date the slot ends" msgstr "Zeit und Datum zu der der Slot endet" -#: AKModel/models.py:1519 +#: AKModel/models.py:1540 msgid "Primary categories" msgstr "Primäre Kategorien" -#: AKModel/models.py:1520 +#: AKModel/models.py:1542 msgid "Categories that should be assigned to this slot primarily" msgstr "Kategorieren, die diesem Slot primär zugewiesen werden sollen" @@ -1241,35 +1249,35 @@ msgstr "AK-CSV-Export" msgid "AK JSON Export" msgstr "AK-JSON-Export" -#: AKModel/views/ak.py:72 +#: AKModel/views/ak.py:69 msgid "AK Wiki Export" msgstr "AK-Wiki-Export" -#: AKModel/views/ak.py:83 AKModel/views/manage.py:55 +#: AKModel/views/ak.py:80 AKModel/views/manage.py:55 msgid "Wishes" msgstr "Wünsche" -#: AKModel/views/ak.py:95 +#: AKModel/views/ak.py:92 msgid "Delete AK Orga Messages" msgstr "AK-Organachrichten löschen" -#: AKModel/views/ak.py:113 +#: AKModel/views/ak.py:110 msgid "AK Orga Messages successfully deleted" msgstr "AK-Organachrichten erfolgreich gelöscht" -#: AKModel/views/ak.py:125 +#: AKModel/views/ak.py:122 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/ak.py:126 +#: AKModel/views/ak.py:123 msgid "Reset of interest in AKs successful." msgstr "Interesse an AKs erfolgreich zurückgesetzt." -#: AKModel/views/ak.py:140 +#: AKModel/views/ak.py:137 msgid "Interest counter of the following AKs will be set to 0:" msgstr "Interessensbekundungszähler der folgenden AKs wird auf 0 gesetzt:" -#: AKModel/views/ak.py:141 +#: AKModel/views/ak.py:138 msgid "AKs' interest counters set back to 0." msgstr "Interessenszähler der AKs zurückgesetzt" diff --git a/AKModel/migrations/0063_field_validators.py b/AKModel/migrations/0063_field_validators.py new file mode 100644 index 0000000000000000000000000000000000000000..347eba289327dd8dc7b8c921f385626c2c725688 --- /dev/null +++ b/AKModel/migrations/0063_field_validators.py @@ -0,0 +1,39 @@ +# Generated by Django 4.2.13 on 2025-03-03 19:59 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('AKModel', '0062_interest_no_history'), + ] + + operations = [ + migrations.AlterField( + model_name='ak', + name='name', + field=models.CharField(help_text='Name of the AK', max_length=256, validators=[django.core.validators.RegexValidator(inverse_match=True, message='May not contain quotation marks', regex='[\'\\"´`]+'), django.core.validators.RegexValidator(message='Must contain at least one letter or digit', regex='[\\w\\s]+')], verbose_name='Name'), + ), + migrations.AlterField( + model_name='ak', + name='short_name', + field=models.CharField(blank=True, help_text='Name displayed in the schedule', max_length=64, validators=[django.core.validators.RegexValidator(inverse_match=True, message='May not contain quotation marks', regex='[\'\\"´`]+')], verbose_name='Short Name'), + ), + migrations.AlterField( + model_name='akowner', + name='name', + field=models.CharField(help_text='Name to identify an AK owner by', max_length=64, validators=[django.core.validators.RegexValidator(inverse_match=True, message='May not contain quotation marks', regex='[\'\\"´`]+'), django.core.validators.RegexValidator(message='Must contain at least one letter or digit', regex='[\\w\\s]+')], verbose_name='Nickname'), + ), + migrations.AlterField( + model_name='historicalak', + name='name', + field=models.CharField(help_text='Name of the AK', max_length=256, validators=[django.core.validators.RegexValidator(inverse_match=True, message='May not contain quotation marks', regex='[\'\\"´`]+'), django.core.validators.RegexValidator(message='Must contain at least one letter or digit', regex='[\\w\\s]+')], verbose_name='Name'), + ), + migrations.AlterField( + model_name='historicalak', + name='short_name', + field=models.CharField(blank=True, help_text='Name displayed in the schedule', max_length=64, validators=[django.core.validators.RegexValidator(inverse_match=True, message='May not contain quotation marks', regex='[\'\\"´`]+')], verbose_name='Short Name'), + ), + ] diff --git a/AKModel/models.py b/AKModel/models.py index e6271580dd69e8167a54f31393dd7e8503d298a3..492be77282ce4ac624cb5569b137ed3a3cd4a796 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -7,7 +7,9 @@ from datetime import datetime, timedelta from typing import Any, Iterable, Generator from django.db import models, transaction +from django.core.validators import RegexValidator from django.apps import apps +from django.db import models from django.db.models import Count from django.urls import reverse_lazy from django.utils import timezone @@ -17,6 +19,16 @@ from simple_history.models import HistoricalRecords from timezone_field import TimeZoneField +# Custom validators to be used for some of the fields +# Prevent inclusion of the quotation marks ' " ´ ` +# This may be necessary to prevent javascript issues +no_quotation_marks_validator = RegexValidator(regex=r"['\"´`]+", inverse_match=True, + message=_('May not contain quotation marks')) +# Enforce that the field contains of at least one letter or digit (and not just special characters +# This prevents issues when autogenerating slugs from that field +slugable_validator = RegexValidator(regex=r"[\w\s]+", message=_('Must contain at least one letter or digit')) + + @dataclass class OptimizerTimeslot: """Class describing a discrete timeslot. Used to interface with an optimizer.""" @@ -132,8 +144,9 @@ class Event(models.Model): help_text=_('When should AKs with intention to submit a resolution be done?')) interest_start = models.DateTimeField(verbose_name=_('Interest Window Start'), blank=True, null=True, - help_text= - _('Opening time for expression of interest. When left blank, no interest indication will be possible.')) + help_text= + _('Opening time for expression of interest. When left blank, no interest ' + 'indication will be possible.')) interest_end = models.DateTimeField(verbose_name=_('Interest Window End'), blank=True, null=True, help_text=_('Closing time for expression of interest.')) @@ -145,7 +158,7 @@ class Event(models.Model): plan_hidden = models.BooleanField(verbose_name=_('Plan Hidden'), help_text=_('Hides plan for non-staff users'), default=True) plan_published_at = models.DateTimeField(verbose_name=_('Plan published at'), blank=True, null=True, - help_text=_('Timestamp at which the plan was published')) + help_text=_('Timestamp at which the plan was published')) base_url = models.URLField(verbose_name=_("Base URL"), help_text=_("Prefix for wiki link construction"), blank=True) wiki_export_template_name = models.CharField(verbose_name=_("Wiki Export Template Name"), blank=True, max_length=50) @@ -159,8 +172,8 @@ class Event(models.Model): contact_email = models.EmailField(verbose_name=_("Contact email address"), blank=True, - help_text=_("An email address that is displayed on every page " - "and can be used for all kinds of questions")) + help_text=_("An email address that is displayed on every page " + "and can be used for all kinds of questions")) class Meta: verbose_name = _('Event') @@ -191,7 +204,7 @@ class Event(models.Model): event = Event.objects.filter(active=True).order_by('start').first() # No active event? Return the next event taking place if event is None: - event = Event.objects.order_by('start').filter(start__gt=datetime.now()).first() + event = Event.objects.order_by('start').filter(start__gt=datetime.now().astimezone()).first() return event def get_categories_with_aks(self, wishes_seperately=False, @@ -646,7 +659,9 @@ class Event(models.Model): class AKOwner(models.Model): """ An AKOwner describes the person organizing/holding an AK. """ - name = models.CharField(max_length=64, verbose_name=_('Nickname'), help_text=_('Name to identify an AK owner by')) + name = models.CharField(max_length=64, verbose_name=_('Nickname'), + validators=[no_quotation_marks_validator, slugable_validator], + help_text=_('Name to identify an AK owner by')) slug = models.SlugField(max_length=64, blank=True, verbose_name=_('Slug'), help_text=_('Slug for URL generation')) institution = models.CharField(max_length=128, blank=True, verbose_name=_('Institution'), help_text=_('Uni etc.')) link = models.URLField(blank=True, verbose_name=_('Web Link'), help_text=_('Link to Homepage')) @@ -725,8 +740,8 @@ class AKCategory(models.Model): description = models.TextField(blank=True, verbose_name=_("Description"), help_text=_("Short description of this AK Category")) present_by_default = models.BooleanField(blank=True, default=True, verbose_name=_("Present by default"), - help_text=_("Present AKs of this category by default " - "if AK owner did not specify whether this AK should be presented?")) + help_text=_("Present AKs of this category by default if AK owner did not " + "specify whether this AK should be presented?")) event = models.ForeignKey(to=Event, on_delete=models.CASCADE, verbose_name=_('Event'), help_text=_('Associated event')) @@ -822,8 +837,10 @@ class AKType(models.Model): class AK(models.Model): """ An AK is a slot-based activity to be scheduled during an event. """ - name = models.CharField(max_length=256, verbose_name=_('Name'), help_text=_('Name of the AK')) + name = models.CharField(max_length=256, verbose_name=_('Name'), help_text=_('Name of the AK'), + validators=[no_quotation_marks_validator, slugable_validator]) short_name = models.CharField(max_length=64, blank=True, verbose_name=_('Short Name'), + validators=[no_quotation_marks_validator], help_text=_('Name displayed in the schedule')) description = models.TextField(blank=True, verbose_name=_('Description'), help_text=_('Description of the AK')) @@ -837,7 +854,7 @@ class AK(models.Model): category = models.ForeignKey(to=AKCategory, on_delete=models.PROTECT, verbose_name=_('Category'), help_text=_('Category of the AK')) types = models.ManyToManyField(to=AKType, blank=True, verbose_name=_('Types'), - help_text=_("This AK is")) + help_text=_("This AK is")) track = models.ForeignKey(to=AKTrack, blank=True, on_delete=models.SET_NULL, null=True, verbose_name=_('Track'), help_text=_('Track the AK belongs to')) @@ -855,8 +872,8 @@ class AK(models.Model): help_text=_('AKs that should precede this AK in the schedule')) notes = models.TextField(blank=True, verbose_name=_('Organizational Notes'), help_text=_( - '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/editing).')) + '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/editing).')) interest = models.IntegerField(default=-1, verbose_name=_('Interest'), help_text=_('Expected number of people')) interest_counter = models.IntegerField(default=0, verbose_name=_('Interest Counter'), @@ -997,7 +1014,7 @@ class AK(models.Model): return reverse_lazy('submit:ak_detail', kwargs={'event_slug': self.event.slug, 'pk': self.id}) return self.edit_url - def save(self, force_insert=False, force_update=False, using=None, update_fields=None): + def save(self, *args, force_insert=False, force_update=False, using=None, update_fields=None): # Auto-Generate Link if not set yet if self.link == "": link = self.event.base_url + self.name.replace(" ", "_") @@ -1006,7 +1023,8 @@ class AK(models.Model): # Tell Django that we have updated the link field if update_fields is not None: update_fields = {"link"}.union(update_fields) - super().save(force_insert, force_update, using, update_fields) + super().save(*args, + force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields) class Room(models.Model): @@ -1161,7 +1179,7 @@ class AKSlot(models.Model): def overlaps(self, other: "AKSlot"): """ - Check wether two slots overlap + Check whether two slots overlap :param other: second slot to compare with :return: true if they overlap, false if not: @@ -1169,13 +1187,14 @@ class AKSlot(models.Model): """ return self.start < other.end <= self.end or self.start <= other.start < self.end - def save(self, force_insert=False, force_update=False, using=None, update_fields=None): + def save(self, *args, force_insert=False, force_update=False, using=None, update_fields=None): # Make sure duration is not longer than the event if update_fields is None or 'duration' in update_fields: event_duration = self.event.end - self.event.start event_duration_hours = event_duration.days * 24 + event_duration.seconds // 3600 self.duration = min(self.duration, event_duration_hours) - super().save(force_insert, force_update, using, update_fields) + super().save(*args, + force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields) def as_json_dict(self) -> dict[str, Any]: """Return a json representation of the AK object of this slot. @@ -1288,6 +1307,7 @@ class ConstraintViolation(models.Model): Depending on the type, different fields (references to other models) will be filled. Each violation should always be related to an event and at least on other instance of a causing entity """ + class Meta: verbose_name = _('Constraint Violation') verbose_name_plural = _('Constraint Violations') @@ -1304,7 +1324,7 @@ class ConstraintViolation(models.Model): AK_CONFLICT_COLLISION = 'acc', _('AK Slot is scheduled at the same time as an AK listed as a conflict') AK_BEFORE_PREREQUISITE = 'abp', _('AK Slot is scheduled before an AK listed as a prerequisite') AK_AFTER_RESODEADLINE = 'aar', _( - 'AK Slot for AK with intention to submit a resolution is scheduled after resolution deadline') + 'AK Slot for AK with intention to submit a resolution is scheduled after resolution deadline') AK_CATEGORY_MISMATCH = 'acm', _('AK Slot in a category is outside that categories availabilities') AK_SLOT_COLLISION = 'asc', _('Two AK Slots for the same AK scheduled at the same time') ROOM_CAPACITY_EXCEEDED = 'rce', _('Room does not have enough space for interest in scheduled AK Slot') @@ -1505,6 +1525,7 @@ class DefaultSlot(models.Model): Model representing a default slot, i.e., a prefered slot to use for typical AKs in the schedule to guarantee enough breaks etc. """ + class Meta: verbose_name = _('Default Slot') verbose_name_plural = _('Default Slots') @@ -1517,7 +1538,8 @@ class DefaultSlot(models.Model): help_text=_('Associated event')) primary_categories = models.ManyToManyField(to=AKCategory, verbose_name=_('Primary categories'), blank=True, - help_text=_('Categories that should be assigned to this slot primarily')) + help_text=_( + 'Categories that should be assigned to this slot primarily')) @property def start_simplified(self) -> str: diff --git a/AKPlan/views.py b/AKPlan/views.py index 4123256e0ceb18c0da87526deaea1a0b46b91b39..a3c240a9a46a32937307f8a7381c14aa3b076c4f 100644 --- a/AKPlan/views.py +++ b/AKPlan/views.py @@ -1,13 +1,12 @@ -from datetime import timedelta +from datetime import datetime, timedelta from django.conf import settings from django.shortcuts import redirect from django.urls import reverse_lazy -from django.utils.datetime_safe import datetime -from django.views.generic import ListView, DetailView +from django.views.generic import DetailView, ListView -from AKModel.models import AKSlot, Room, AKTrack from AKModel.metaviews.admin import FilterByEventSlugMixin +from AKModel.models import AKSlot, AKTrack, Room class PlanIndexView(FilterByEventSlugMixin, ListView): @@ -152,7 +151,7 @@ class PlanTrackView(FilterByEventSlugMixin, DetailView): context = super().get_context_data(object_list=object_list, **kwargs) # Restrict AKSlot list to given track # while joining AK, room and category information to reduce the amount of necessary SQL queries - context["slots"] = AKSlot.objects.\ - filter(event=self.event, ak__track=context['track']).\ + context["slots"] = AKSlot.objects. \ + filter(event=self.event, ak__track=context['track']). \ select_related('ak', 'room', 'ak__category') return context diff --git a/AKSubmission/api.py b/AKSubmission/api.py index 39c3fd460a404735e66d690746c8f65e892b1137..2e82f4e080b1e14c668c048b1ccc3fc0672ec715 100644 --- a/AKSubmission/api.py +++ b/AKSubmission/api.py @@ -1,7 +1,8 @@ +from datetime import datetime + from rest_framework import status from rest_framework.decorators import api_view from rest_framework.response import Response -from django.utils.datetime_safe import datetime from AKModel.models import AK diff --git a/AKSubmission/locale/de_DE/LC_MESSAGES/django.po b/AKSubmission/locale/de_DE/LC_MESSAGES/django.po index f47345a6b052651a9a8e049f95a41be97b463f07..f2c47bb8cc952859563b7df1144ee77d7e036017 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: 2025-02-27 15:13+0000\n" +"POT-Creation-Date: 2025-03-04 17:35+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -274,7 +274,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:84 +#: AKSubmission/templates/AKSubmission/ak_list.html:6 AKSubmission/views.py:82 msgid "All AKs" msgstr "Alle AKs" @@ -400,78 +400,78 @@ msgstr "" msgid "Submit" msgstr "Eintragen" -#: AKSubmission/views.py:127 +#: AKSubmission/views.py:125 msgid "Wishes" msgstr "Wünsche" -#: AKSubmission/views.py:127 +#: AKSubmission/views.py:125 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:169 +#: AKSubmission/views.py:167 msgid "Currently planned AKs" msgstr "Aktuell geplante AKs" -#: AKSubmission/views.py:233 +#: AKSubmission/views.py:235 msgid "AKs with Track" msgstr "AK mit Track" -#: AKSubmission/views.py:302 +#: AKSubmission/views.py:305 msgid "Event inactive. Cannot create or update." msgstr "Event inaktiv. Hinzufügen/Bearbeiten nicht möglich." -#: AKSubmission/views.py:327 +#: AKSubmission/views.py:330 msgid "AK successfully created" msgstr "AK erfolgreich angelegt" -#: AKSubmission/views.py:400 +#: AKSubmission/views.py:404 msgid "AK successfully updated" msgstr "AK erfolgreich aktualisiert" -#: AKSubmission/views.py:451 +#: AKSubmission/views.py:455 #, 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:555 +#: AKSubmission/views.py:558 msgid "No user selected" msgstr "Keine Person ausgewählt" -#: AKSubmission/views.py:571 +#: AKSubmission/views.py:574 msgid "Person Info successfully updated" msgstr "Personen-Info erfolgreich aktualisiert" -#: AKSubmission/views.py:607 +#: AKSubmission/views.py:610 msgid "AK Slot successfully added" msgstr "AK-Slot erfolgreich angelegt" -#: AKSubmission/views.py:626 +#: AKSubmission/views.py:629 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:636 +#: AKSubmission/views.py:639 msgid "AK Slot successfully updated" msgstr "AK-Slot erfolgreich aktualisiert" -#: AKSubmission/views.py:654 +#: AKSubmission/views.py:657 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:664 +#: AKSubmission/views.py:667 msgid "AK Slot successfully deleted" msgstr "AK-Slot erfolgreich angelegt" -#: AKSubmission/views.py:676 +#: AKSubmission/views.py:679 msgid "Messages" msgstr "Nachrichten" -#: AKSubmission/views.py:686 +#: AKSubmission/views.py:689 msgid "Delete all messages" msgstr "Alle Nachrichten löschen" -#: AKSubmission/views.py:713 +#: AKSubmission/views.py:716 msgid "Message to organizers successfully saved" msgstr "Nachricht an die Organisator*innen erfolgreich gespeichert" diff --git a/AKSubmission/tests.py b/AKSubmission/tests.py index 8180ea3c10a4b5d28d741467bbc6df4906f25003..fd31454391bbc15ed42ba5e77a1e6da5b04f1461 100644 --- a/AKSubmission/tests.py +++ b/AKSubmission/tests.py @@ -1,8 +1,7 @@ -from datetime import timedelta +from datetime import datetime, timedelta from django.test import TestCase from django.urls import reverse_lazy -from django.utils.datetime_safe import datetime from AKModel.models import AK, AKSlot, Event from AKModel.tests.test_views import BasicViewTests @@ -47,8 +46,8 @@ class ModelViewTests(BasicViewTests, TestCase): 'expected_message': "AK successfully updated"}, {'view': 'akslot_edit', 'target_view': 'ak_detail', 'kwargs': {'event_slug': 'kif42', 'pk': 5}, 'target_kwargs': {'event_slug': 'kif42', 'pk': 1}, 'expected_message': "AK Slot successfully updated"}, - {'view': 'akowner_edit', 'target_view': 'submission_overview', 'kwargs': {'event_slug': 'kif42', 'slug': 'a'}, - 'target_kwargs': {'event_slug': 'kif42'}, 'expected_message': "Person Info successfully updated"}, + {'view': 'akowner_edit', 'target_view': 'submission_overview', 'kwargs': {'event_slug': 'kif42', 'slug': 'a'}, + 'target_kwargs': {'event_slug': 'kif42'}, 'expected_message': "Person Info successfully updated"}, ] def test_akslot_edit_delete_prevention(self): @@ -147,7 +146,8 @@ class ModelViewTests(BasicViewTests, TestCase): add_redirect_url = reverse_lazy(f"{self.APP_NAME}:submit_ak", kwargs={'event_slug': 'kif42', 'owner_slug': 'a'}) response = self.client.post(select_url, {'owner_id': 1}) self.assertRedirects(response, add_redirect_url, status_code=302, target_status_code=200, - msg_prefix=f"Dispatch redirect to ak submission page failed (should go to {add_redirect_url})") + msg_prefix=f"Dispatch redirect to ak submission page failed " + f"(should go to {add_redirect_url})") def test_orga_message_submission(self): """ @@ -201,7 +201,8 @@ class ModelViewTests(BasicViewTests, TestCase): # Test indication outside of indication window -> HTTP 403, counter not increased response = self.client.post(interest_api_url) self.assertEqual(response.status_code, 403, - "API end point still reachable even though interest indication window ended ({interest_api_url})") + "API end point still reachable even though interest indication window ended " + "({interest_api_url})") self.assertEqual(AK.objects.get(pk=1).interest_counter, ak_interest_counter + 1, "Counter was increased even though interest indication window ended") @@ -243,13 +244,14 @@ class ModelViewTests(BasicViewTests, TestCase): Test visibility of requirements field in submission form """ event = Event.get_by_slug('kif42') - form = AKSubmissionForm(data={'name': 'Test AK', 'event': event}, instance=None, initial={"event":event}) + form = AKSubmissionForm(data={'name': 'Test AK', 'event': event}, instance=None, initial={"event": event}) self.assertIn('requirements', form.fields, msg="Requirements field not present in form even though event has requirements") event2 = Event.objects.create(name='Event without requirements', slug='no_req', - start=datetime.now(), end=datetime.now(), + start=datetime.now().astimezone(event.timezone), + end=datetime.now().astimezone(event.timezone), active=True) form2 = AKSubmissionForm(data={'name': 'Test AK', 'event': event2}, instance=None, initial={"event": event2}) self.assertNotIn('requirements', form2.fields, @@ -260,13 +262,14 @@ class ModelViewTests(BasicViewTests, TestCase): Test visibility of types field in submission form """ event = Event.get_by_slug('kif42') - form = AKSubmissionForm(data={'name': 'Test AK', 'event': event}, instance=None, initial={"event":event}) + form = AKSubmissionForm(data={'name': 'Test AK', 'event': event}, instance=None, initial={"event": event}) self.assertIn('types', form.fields, msg="Requirements field not present in form even though event has requirements") event2 = Event.objects.create(name='Event without types', slug='no_types', - start=datetime.now(), end=datetime.now(), + start=datetime.now().astimezone(event.timezone), + end=datetime.now().astimezone(event.timezone), active=True) form2 = AKSubmissionForm(data={'name': 'Test AK', 'event': event2}, instance=None, initial={"event": event2}) self.assertNotIn('types', form2.fields, diff --git a/AKSubmission/views.py b/AKSubmission/views.py index 5263b875ea21399a5f98d37914403c2f674ec391..3bbffa1f55da2c52e635f6349642de921d6b3cad 100644 --- a/AKSubmission/views.py +++ b/AKSubmission/views.py @@ -1,6 +1,6 @@ -from datetime import timedelta -from math import floor from abc import ABC, abstractmethod +from datetime import datetime, timedelta +from math import floor from django.apps import apps from django.conf import settings @@ -8,19 +8,17 @@ from django.contrib import messages from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, redirect from django.urls import reverse_lazy -from django.utils.datetime_safe import datetime from django.utils.translation import gettext_lazy as _ from django.views import View -from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView, TemplateView +from django.views.generic import CreateView, DeleteView, DetailView, ListView, TemplateView, UpdateView 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.metaviews.admin import EventSlugMixin, FilterByEventSlugMixin +from AKModel.metaviews.status import TemplateStatusWidget +from AKModel.models import AK, AKCategory, AKOrgaMessage, AKOwner, AKSlot, AKTrack from AKSubmission.api import ak_interest_indication_active -from AKSubmission.forms import AKWishForm, AKOwnerForm, AKSubmissionForm, AKDurationForm, AKOrgaMessageForm, \ - AKForm +from AKSubmission.forms import AKDurationForm, AKForm, AKOrgaMessageForm, AKOwnerForm, AKSubmissionForm, AKWishForm class SubmissionErrorNotConfiguredView(EventSlugMixin, TemplateView): @@ -47,7 +45,7 @@ class AKOverviewView(FilterByEventSlugMixin, ListView): template_name = "AKSubmission/ak_overview.html" wishes_as_category = False - def filter_aks(self, context, category): # pylint: disable=unused-argument + def filter_aks(self, context, category): # pylint: disable=unused-argument """ Filter which AKs to display based on the given context and category @@ -73,7 +71,7 @@ class AKOverviewView(FilterByEventSlugMixin, ListView): """ return context["categories_with_aks"][0][0].name - def get_table_title(self, context): # pylint: disable=unused-argument + def get_table_title(self, context): # pylint: disable=unused-argument """ Specify the title above the AK list/table in this view @@ -91,7 +89,7 @@ class AKOverviewView(FilterByEventSlugMixin, ListView): redirect to error page if necessary (see :class:`SubmissionErrorNotConfiguredView`) """ self._load_event() - self.object_list = self.get_queryset() # pylint: disable=attribute-defined-outside-init + self.object_list = self.get_queryset() # pylint: disable=attribute-defined-outside-init # No categories yet? Redirect to configuration error page if self.object_list.count() == 0: @@ -124,7 +122,7 @@ class AKOverviewView(FilterByEventSlugMixin, ListView): if self.wishes_as_category: categories_with_aks.append( - (AKCategory(name=_("Wishes"), pk=0, description=_("AKs one would like to have")), ak_wishes)) + (AKCategory(name=_("Wishes"), pk=0, description=_("AKs one would like to have")), ak_wishes)) context["categories_with_aks"] = categories_with_aks context["active_category"] = self.get_active_category_name(context) @@ -183,10 +181,13 @@ class AKListByCategoryView(AKOverviewView): This view inherits from :class:`AKOverviewView`, but produces only one list instead of a tabbed one. """ + def dispatch(self, request, *args, **kwargs): # Override dispatching # Needed to handle the checking whether the category exists - self.category = get_object_or_404(AKCategory, pk=kwargs['category_pk']) # pylint: disable=attribute-defined-outside-init,line-too-long + # noinspection PyAttributeOutsideInit + # pylint: disable=attribute-defined-outside-init + self.category = get_object_or_404(AKCategory, pk=kwargs['category_pk']) return super().dispatch(request, *args, **kwargs) def get_active_category_name(self, context): @@ -209,11 +210,12 @@ class AKListByTrackView(AKOverviewView): This view inherits from :class:`AKOverviewView` and there will be one list per category -- but only AKs of a certain given track will be included in them. """ + def dispatch(self, request, *args, **kwargs): # Override dispatching # Needed to handle the checking whether the track exists - self.track = get_object_or_404(AKTrack, pk=kwargs['track_pk']) # pylint: disable=attribute-defined-outside-init + self.track = get_object_or_404(AKTrack, pk=kwargs['track_pk']) # pylint: disable=attribute-defined-outside-init return super().dispatch(request, *args, **kwargs) def filter_aks(self, context, category): @@ -292,6 +294,7 @@ class EventInactiveRedirectMixin: Will add a message explaining why the action was not performed to the user and then redirect to start page of the submission component """ + def get_error_message(self): """ Error message to display after redirect (can be adjusted by this method) @@ -351,6 +354,7 @@ class AKSubmissionView(AKAndAKWishSubmissionView): Extends :class:`AKAndAKWishSubmissionView` """ + def get_initial(self): # Load initial values for the form # Used to directly add the first owner and the event this AK will belong to @@ -500,7 +504,6 @@ class AKOwnerDispatchView(ABC, EventSlugMixin, View): :rtype: HttpResponseRedirect """ - def post(self, request, *args, **kwargs): # This view is solely meant to handle POST requests # Perform dispatching based on the submitted owner_id diff --git a/INSTALL.md b/INSTALL.md index 5344a998cffe2603aaaffb0583e91fb43e533c38..795ff71a815cd78cf699f829098882c436e522c4 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -10,7 +10,7 @@ setup. ### System Requirements -* Python 3.10+ incl. development tools +* Python3.11+ incl. development tools * Virtualenv * pdflatex & beamer class (`texlive-latex-base texlive-latex-recommended texlive-latex-extra texlive-fonts-extra texlive-luatex`) diff --git a/Utils/check.sh b/Utils/check.sh index c2e09f5b8852b9a2e70b9c9f6f9e1296c1404f02..a8a9d4b7a9d6e78e7d93cabecb6bd4f36af4f89f 100755 --- a/Utils/check.sh +++ b/Utils/check.sh @@ -8,14 +8,21 @@ if [ -z ${VIRTUAL_ENV+x} ]; then fi # enable really all warnings, some of them are silenced by default -if [[ "$@" == *"--all"* ]]; then - export PYTHONWARNINGS=all -fi +for arg in "$@"; do + if [[ "$arg" == "--all" ]]; then + export PYTHONWARNINGS=all + fi +done # in case of checking production setup -if [[ "$@" == *"--prod"* ]]; then - export DJANGO_SETTINGS_MODULE=AKPlanning.settings_production - ./manage.py check --deploy -fi +for arg in "$@"; do + if [[ "$arg" == "--prod" ]]; then + export DJANGO_SETTINGS_MODULE=AKPlanning.settings_production + ./manage.py check --deploy + ./manage.py makemigrations --dry-run --check + fi +done +# check the setup ./manage.py check +./manage.py makemigrations --dry-run --check diff --git a/Utils/setup.sh b/Utils/setup.sh index 6a93207d197e75da2875b31ea8e0e631e114e837..1123bbd8ae707b4b15d2aef6ec33a0ec68a1e2e9 100755 --- a/Utils/setup.sh +++ b/Utils/setup.sh @@ -10,7 +10,7 @@ rm -rf venv/ # Setup Python Environment # Requires: Virtualenv, appropriate Python installation -virtualenv venv -p python3.10 +virtualenv venv -p python3.11 source venv/bin/activate pip install --upgrade setuptools pip wheel pip install -r requirements.txt diff --git a/Utils/test.sh b/Utils/test.sh new file mode 100644 index 0000000000000000000000000000000000000000..ab6dbb2b9f23f3427b773715c5fa4ba1d74c179e --- /dev/null +++ b/Utils/test.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Test the AKPlanning setup +# execute as Utils/test.sh + +# activate virtualenv when necessary +if [ -z ${VIRTUAL_ENV+x} ]; then + source venv/bin/activate +fi + +# enable really all warnings, some of them are silenced by default +for arg in "$@"; do + if [[ "$arg" == "--all" ]]; then + export PYTHONWARNINGS=all + fi +done + +# in case of checking production setup +for arg in "$@"; do + if [[ "$arg" == "--prod" ]]; then + export DJANGO_SETTINGS_MODULE=AKPlanning.settings_production + ./manage.py test --deploy + fi +done + +# run tests +./manage.py test diff --git a/requirements.txt b/requirements.txt index 451feef4db2ee295a38687bc0f96ff1dee637418..9b0938d4a76aff97222757ebd6ac30ac3efef24a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,26 +1,27 @@ -Django==4.2.13 -django-bootstrap5==24.2 -fontawesomefree==6.5.1 # Makes static files (css, fonts) available locally -django-fontawesome-6==1.0.0.0 # Provides an icon field for models and forms as well as handy shortcuts to render icons -django-split-settings==1.3.1 -django-timezone-field==6.1.0 -djangorestframework==3.15.1 -django-simple-history==3.5.0 -django-registration-redux==2.13 -django-debug-toolbar==4.3.0 +Django==5.1.6 +django-betterforms==2.0.0 django-bootstrap-datepicker-plus==5.0.5 -django-tex==1.1.10 -django-csp==3.8 -django-compressor==4.4 +django-bootstrap5==25.1 +django-compressor==4.5.1 +django-debug-toolbar==5.0.1 +django-fontawesome-6==1.0.0.0 # Provides an icon field for models and forms as well as handy shortcuts to render icons django-libsass==0.9 -django-betterforms==2.0.0 -mysqlclient==2.2.0 # for production deployment -tzdata==2024.1 +django-registration-redux==2.13 +django-simple-history==3.8.0 +django-split-settings==1.3.2 +django-tex==1.1.10 +django-timezone-field==7.1 +django_csp==3.8 +djangorestframework==3.15.2 + +fontawesomefree==6.6.0 # Makes static files (css, fonts) available locally +mysqlclient==2.2.7 # for production deployment +tzdata==2025.1 # Documentation -sphinxcontrib-django==2.5 +Sphinx==8.2.3 +sphinx-rtd-theme==3.0.2 sphinxcontrib-apidoc==0.5.0 +sphinxcontrib-django==2.5.0 recommonmark==0.7.1 django-docs==0.3.3 -sphinx-rtd-theme==2.0.0 -sphinx==7.3.7