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..5cb605d57647a7083c60cb64af9db563b906601c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: python:3.10 +image: python:3.11 services: - mysql @@ -26,22 +26,18 @@ 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 test: extends: .before_script_template script: - - source venv/bin/activate - echo "GRANT ALL on *.* to '${MYSQL_USER}';"| mysql -u root --password="${MYSQL_ROOT_PASSWORD}" -h mysql - - pip install pytest-cov unittest-xml-reporting beautifulsoup4 + - pip install pytest-cov unittest-xml-reporting - coverage run --source='.' manage.py test --settings AKPlanning.settings_ci after_script: - - source venv/bin/activate - coverage report - coverage xml coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/' @@ -56,8 +52,6 @@ lint: extends: .before_script_template stage: test script: - - source venv/bin/activate - - pip install beautifulsoup4 - pylint --load-plugins pylint_django --django-settings-module=AKPlanning.settings_ci --rcfile pylintrc --exit-zero --output-format=text AK* | tee /tmp/pylint.txt - sed -n 's/^Your code has been rated at \([-0-9.]*\)\/.*/\1/p' /tmp/pylint.txt > public/badges/$CI_JOB_NAME.score - pylint --load-plugins pylint_django --django-settings-module=AKPlanning.settings_ci --rcfile pylintrc --exit-zero --output-format=pylint_gitlab.GitlabCodeClimateReporter AK* > codeclimate.json 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..9ef95b03bc63a8bf36540a2e4b674d6c9e2b259a 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 14:49+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:890 AKModel/models.py:1348 +#: 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:177 +#: AKModel/models.py:667 AKModel/models.py:744 AKModel/models.py:777 +#: AKModel/models.py:803 AKModel/models.py:822 AKModel/models.py:880 +#: AKModel/models.py:1039 AKModel/models.py:1116 AKModel/models.py:1286 +#: AKModel/models.py:1344 AKModel/models.py:1536 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:668 +#: AKModel/models.py:745 AKModel/models.py:778 AKModel/models.py:804 +#: AKModel/models.py:823 AKModel/models.py:881 AKModel/models.py:1040 +#: AKModel/models.py:1117 AKModel/models.py:1287 AKModel/models.py:1345 +#: AKModel/models.py:1537 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:1043 +#: AKModel/models.py:1106 AKModel/models.py:1354 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:889 +#: AKModel/models.py:1105 AKModel/models.py:1281 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:748 +#: AKModel/models.py:1360 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 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:1530 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:140 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:24 +msgid "May not contain quotation marks" +msgstr "Darf keine Anführungszeichen enthalten" + +#: AKModel/models.py:27 +msgid "Must contain at least one letter or digit" +msgstr "Muss mindestens einen Buchstaben oder eine Ziffer enthalten" + +#: AKModel/models.py:131 AKModel/models.py:736 AKModel/models.py:774 +#: AKModel/models.py:801 AKModel/models.py:820 AKModel/models.py:838 +#: AKModel/models.py:1031 msgid "Name" msgstr "Name" -#: AKModel/models.py:121 +#: AKModel/models.py:132 msgid "Name or iteration of the event" msgstr "Name oder Iteration des Events" -#: AKModel/models.py:122 +#: AKModel/models.py:133 msgid "Short Form" msgstr "Kurzer Name" -#: AKModel/models.py:123 +#: AKModel/models.py:134 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:136 msgid "Place" msgstr "Ort" -#: AKModel/models.py:126 +#: AKModel/models.py:137 msgid "City etc. the event takes place in" msgstr "Stadt o.ä. in der das Event stattfindet" -#: AKModel/models.py:128 +#: AKModel/models.py:139 msgid "Time Zone" msgstr "Zeitzone" -#: AKModel/models.py:128 +#: AKModel/models.py:139 msgid "Time Zone where this event takes place in" msgstr "Zeitzone in der das Event stattfindet" -#: AKModel/models.py:129 +#: AKModel/models.py:140 msgid "Time the event begins" msgstr "Zeit zu der das Event beginnt" -#: AKModel/models.py:130 +#: AKModel/models.py:141 msgid "End" msgstr "Ende" -#: AKModel/models.py:130 +#: AKModel/models.py:141 msgid "Time the event ends" msgstr "Zeit zu der das Event endet" -#: AKModel/models.py:131 +#: AKModel/models.py:142 msgid "Resolution Deadline" msgstr "Resolutionsdeadline" -#: AKModel/models.py:132 +#: AKModel/models.py:143 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:145 msgid "Interest Window Start" msgstr "Beginn Interessensbekundung" -#: AKModel/models.py:136 +#: AKModel/models.py:147 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:150 msgid "Interest Window End" msgstr "Ende Interessensbekundung" -#: AKModel/models.py:139 +#: AKModel/models.py:151 msgid "Closing time for expression of interest." msgstr "Öffnungszeitpunkt für die Angabe von Interesse an AKs." -#: AKModel/models.py:141 +#: AKModel/models.py:153 msgid "Public event" msgstr "Öffentliches Event" -#: AKModel/models.py:142 +#: AKModel/models.py:154 msgid "Show this event on overview page." msgstr "Zeige dieses Event auf der Übersichtseite an" -#: AKModel/models.py:144 +#: AKModel/models.py:156 msgid "Active State" msgstr "Aktiver Status" -#: AKModel/models.py:144 +#: AKModel/models.py:156 msgid "Marks currently active events" msgstr "Markiert aktuell aktive Events" -#: AKModel/models.py:145 +#: AKModel/models.py:157 msgid "Plan Hidden" msgstr "Plan verborgen" -#: AKModel/models.py:145 +#: AKModel/models.py:157 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:159 msgid "Plan published at" msgstr "Plan veröffentlicht am/um" -#: AKModel/models.py:148 +#: AKModel/models.py:160 msgid "Timestamp at which the plan was published" msgstr "Zeitpunkt, zu dem der Plan veröffentlicht wurde" -#: AKModel/models.py:150 +#: AKModel/models.py:162 msgid "Base URL" msgstr "URL-Prefix" -#: AKModel/models.py:150 +#: AKModel/models.py:162 msgid "Prefix for wiki link construction" msgstr "Prefix für die automatische Generierung von Wiki-Links" -#: AKModel/models.py:151 +#: AKModel/models.py:163 msgid "Wiki Export Template Name" msgstr "Wiki-Export Templatename" -#: AKModel/models.py:152 +#: AKModel/models.py:164 msgid "Default Slot Length" msgstr "Standardslotlänge" -#: AKModel/models.py:153 +#: AKModel/models.py:165 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:166 msgid "Export Slot Length" msgstr "Export-Slotlänge" -#: AKModel/models.py:156 +#: AKModel/models.py:168 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:172 msgid "Contact email address" msgstr "E-Mail Kontaktadresse" -#: AKModel/models.py:162 +#: AKModel/models.py:173 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:178 msgid "Events" msgstr "Events" -#: AKModel/models.py:444 +#: AKModel/models.py:455 #, 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:465 #, 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:479 #, 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:490 #, 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:660 msgid "Nickname" msgstr "Spitzname" -#: AKModel/models.py:649 +#: AKModel/models.py:662 msgid "Name to identify an AK owner by" msgstr "Name, durch den eine AK-Leitung identifiziert wird" -#: AKModel/models.py:650 +#: AKModel/models.py:663 msgid "Slug" msgstr "Slug" -#: AKModel/models.py:650 +#: AKModel/models.py:663 msgid "Slug for URL generation" msgstr "Slug für URL-Generierung" -#: AKModel/models.py:651 +#: AKModel/models.py:664 msgid "Institution" msgstr "Instutution" -#: AKModel/models.py:651 +#: AKModel/models.py:664 msgid "Uni etc." msgstr "Universität o.ä." -#: AKModel/models.py:652 AKModel/models.py:834 +#: AKModel/models.py:665 AKModel/models.py:849 msgid "Web Link" msgstr "Internet Link" -#: AKModel/models.py:652 +#: AKModel/models.py:665 msgid "Link to Homepage" msgstr "Link zu Homepage oder Webseite" -#: AKModel/models.py:658 AKModel/models.py:1334 +#: AKModel/models.py:671 AKModel/models.py:1353 msgid "AK Owner" msgstr "AK-Leitung" -#: AKModel/models.py:659 +#: AKModel/models.py:672 msgid "AK Owners" msgstr "AK-Leitungen" -#: AKModel/models.py:723 +#: AKModel/models.py:736 msgid "Name of the AK Category" msgstr "Name der AK-Kategorie" -#: AKModel/models.py:724 AKModel/models.py:762 +#: AKModel/models.py:737 AKModel/models.py:775 msgid "Color" msgstr "Farbe" -#: AKModel/models.py:724 AKModel/models.py:762 +#: AKModel/models.py:737 AKModel/models.py:775 msgid "Color for displaying" msgstr "Farbe für die Anzeige" -#: AKModel/models.py:725 AKModel/models.py:828 +#: AKModel/models.py:738 AKModel/models.py:843 msgid "Description" msgstr "Beschreibung" -#: AKModel/models.py:726 +#: AKModel/models.py:739 msgid "Short description of this AK Category" msgstr "Beschreibung der AK-Kategorie" -#: AKModel/models.py:727 +#: AKModel/models.py:740 msgid "Present by default" msgstr "Defaultmäßig präsentieren" -#: AKModel/models.py:728 +#: AKModel/models.py:741 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:749 msgid "AK Categories" msgstr "AK-Kategorien" -#: AKModel/models.py:761 +#: AKModel/models.py:774 msgid "Name of the AK Track" msgstr "Name des AK-Tracks" -#: AKModel/models.py:768 +#: AKModel/models.py:781 msgid "AK Track" msgstr "AK-Track" -#: AKModel/models.py:769 +#: AKModel/models.py:782 msgid "AK Tracks" msgstr "AK-Tracks" -#: AKModel/models.py:788 +#: AKModel/models.py:801 msgid "Name of the Requirement" msgstr "Name der Anforderung" -#: AKModel/models.py:794 AKModel/models.py:1338 +#: AKModel/models.py:807 AKModel/models.py:1357 msgid "AK Requirement" msgstr "AK-Anforderung" -#: AKModel/models.py:795 +#: AKModel/models.py:808 msgid "AK Requirements" msgstr "AK-Anforderungen" -#: AKModel/models.py:807 +#: AKModel/models.py:820 msgid "Name describing the type" msgstr "Name, der den Typ beschreibt" -#: AKModel/models.py:813 +#: AKModel/models.py:826 msgid "AK Type" msgstr "AK Typ" -#: AKModel/models.py:814 +#: AKModel/models.py:827 msgid "AK Types" msgstr "AK-Typen" -#: AKModel/models.py:825 +#: AKModel/models.py:838 msgid "Name of the AK" msgstr "Name des AKs" -#: AKModel/models.py:826 +#: AKModel/models.py:840 msgid "Short Name" msgstr "Kurzer Name" -#: AKModel/models.py:827 +#: AKModel/models.py:842 msgid "Name displayed in the schedule" msgstr "Name zur Anzeige im AK-Plan" -#: AKModel/models.py:828 +#: AKModel/models.py:843 msgid "Description of the AK" msgstr "Beschreibung des AKs" -#: AKModel/models.py:830 +#: AKModel/models.py:845 msgid "Owners" msgstr "Leitungen" -#: AKModel/models.py:831 +#: AKModel/models.py:846 msgid "Those organizing the AK" msgstr "Menschen, die den AK organisieren und halten" -#: AKModel/models.py:834 +#: AKModel/models.py:849 msgid "Link to wiki page" msgstr "Link zur Wiki Seite" -#: AKModel/models.py:835 +#: AKModel/models.py:850 msgid "Protocol Link" msgstr "Protokolllink" -#: AKModel/models.py:835 +#: AKModel/models.py:850 msgid "Link to protocol" msgstr "Link zum Protokoll" -#: AKModel/models.py:837 +#: AKModel/models.py:852 msgid "Category" msgstr "Kategorie" -#: AKModel/models.py:838 +#: AKModel/models.py:853 msgid "Category of the AK" msgstr "Kategorie des AKs" -#: AKModel/models.py:839 AKModel/models.py:904 +#: AKModel/models.py:854 msgid "Types" msgstr "Typen" -#: AKModel/models.py:840 +#: AKModel/models.py:855 msgid "This AK is" msgstr "Dieser AK ist" -#: AKModel/models.py:841 +#: AKModel/models.py:856 msgid "Track" msgstr "Track" -#: AKModel/models.py:842 +#: AKModel/models.py:857 msgid "Track the AK belongs to" msgstr "Track zu dem der AK gehört" -#: AKModel/models.py:844 +#: AKModel/models.py:859 msgid "Resolution Intention" msgstr "Resolutionsabsicht" -#: AKModel/models.py:845 +#: AKModel/models.py:860 msgid "Intends to submit a resolution" msgstr "Beabsichtigt eine Resolution einzureichen" -#: AKModel/models.py:846 +#: AKModel/models.py:861 msgid "Present this AK" msgstr "AK präsentieren" -#: AKModel/models.py:847 +#: AKModel/models.py:862 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:864 AKModel/views/status.py:175 msgid "Requirements" msgstr "Anforderungen" -#: AKModel/models.py:850 +#: AKModel/models.py:865 msgid "AK's Requirements" msgstr "Anforderungen des AKs" -#: AKModel/models.py:852 +#: AKModel/models.py:867 msgid "Conflicting AKs" msgstr "AK-Konflikte" -#: AKModel/models.py:853 +#: AKModel/models.py:868 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:869 msgid "Prerequisite AKs" msgstr "Vorausgesetzte AKs" -#: AKModel/models.py:855 +#: AKModel/models.py:870 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:872 msgid "Organizational Notes" msgstr "Notizen zur Organisation" -#: AKModel/models.py:858 +#: AKModel/models.py:873 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,303 +738,295 @@ msgstr "" "Anmerkungen bitte den Button für Direktnachrichten verwenden (nach dem " "Anlegen/Bearbeiten)." -#: AKModel/models.py:861 AKModel/models.py:900 +#: AKModel/models.py:876 msgid "Interest" msgstr "Interesse" -#: AKModel/models.py:861 +#: AKModel/models.py:876 msgid "Expected number of people" msgstr "Erwartete Personenzahl" -#: AKModel/models.py:862 +#: AKModel/models.py:877 msgid "Interest Counter" msgstr "Interessenszähler" -#: AKModel/models.py:863 +#: AKModel/models.py:878 msgid "People who have indicated interest online" msgstr "Anzahl Personen, die online Interesse bekundet haben" -#: AKModel/models.py:868 +#: AKModel/models.py:883 msgid "Export?" msgstr "Export?" -#: AKModel/models.py:869 +#: AKModel/models.py:884 msgid "Include AK in wiki export?" msgstr "AK bei Wiki-Export berücksichtigen?" -#: AKModel/models.py:919 -msgid "Conflicts" -msgstr "Konflikte" - -#: AKModel/models.py:922 -msgid "Prerequisites" -msgstr "Voraussetzungen" - -#: AKModel/models.py:1015 +#: AKModel/models.py:1031 msgid "Name or number of the room" msgstr "Name oder Nummer des Raums" -#: AKModel/models.py:1016 +#: AKModel/models.py:1032 msgid "Location" msgstr "Ort" -#: AKModel/models.py:1017 +#: AKModel/models.py:1033 msgid "Name or number of the location" msgstr "Name oder Nummer des Ortes" -#: AKModel/models.py:1018 +#: AKModel/models.py:1034 msgid "Capacity" msgstr "Kapazität" -#: AKModel/models.py:1019 +#: AKModel/models.py:1035 msgid "Maximum number of people (-1 for unlimited)." msgstr "Maximale Personenzahl (-1 wenn unbeschränkt)." -#: AKModel/models.py:1020 +#: AKModel/models.py:1036 msgid "Properties" msgstr "Eigenschaften" -#: AKModel/models.py:1021 +#: AKModel/models.py:1037 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:1044 AKModel/views/status.py:59 msgid "Rooms" msgstr "Räume" -#: AKModel/models.py:1089 +#: AKModel/models.py:1105 msgid "AK being mapped" msgstr "AK, der zugeordnet wird" -#: AKModel/models.py:1091 +#: AKModel/models.py:1107 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:1108 AKModel/models.py:1533 msgid "Slot Begin" msgstr "Beginn des Slots" -#: AKModel/models.py:1092 AKModel/models.py:1513 +#: AKModel/models.py:1108 AKModel/models.py:1533 msgid "Time and date the slot begins" msgstr "Zeit und Datum zu der der AK beginnt" -#: AKModel/models.py:1094 +#: AKModel/models.py:1110 msgid "Duration" msgstr "Dauer" -#: AKModel/models.py:1095 +#: AKModel/models.py:1111 msgid "Length in hours" msgstr "Länge in Stunden" -#: AKModel/models.py:1097 +#: AKModel/models.py:1113 msgid "Scheduling fixed" msgstr "Planung fix" -#: AKModel/models.py:1098 +#: AKModel/models.py:1114 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:1119 msgid "Last update" msgstr "Letzte Aktualisierung" -#: AKModel/models.py:1106 +#: AKModel/models.py:1122 msgid "AK Slot" msgstr "AK-Slot" -#: AKModel/models.py:1107 AKModel/models.py:1331 AKModel/models.py:1366 +#: AKModel/models.py:1123 AKModel/models.py:1350 msgid "AK Slots" msgstr "AK-Slot" -#: AKModel/models.py:1129 AKModel/models.py:1138 +#: AKModel/models.py:1145 AKModel/models.py:1154 msgid "Not scheduled yet" msgstr "Noch nicht geplant" -#: AKModel/models.py:1264 +#: AKModel/models.py:1282 msgid "AK this message belongs to" msgstr "AK zu dem die Nachricht gehört" -#: AKModel/models.py:1265 +#: AKModel/models.py:1283 msgid "Message text" msgstr "Nachrichtentext" -#: AKModel/models.py:1266 +#: AKModel/models.py:1284 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:1288 msgid "Resolved" msgstr "Erledigt" -#: AKModel/models.py:1271 +#: AKModel/models.py:1289 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:1292 msgid "AK Orga Message" msgstr "AK-Organachricht" -#: AKModel/models.py:1275 +#: AKModel/models.py:1293 msgid "AK Orga Messages" msgstr "AK-Organachrichten" -#: AKModel/models.py:1292 +#: AKModel/models.py:1311 msgid "Constraint Violation" msgstr "Constraintverletzung" -#: AKModel/models.py:1293 +#: AKModel/models.py:1312 msgid "Constraint Violations" msgstr "Constraintverletzungen" -#: AKModel/models.py:1300 +#: AKModel/models.py:1319 msgid "Owner has two parallel slots" msgstr "Leitung hat zwei Slots parallel" -#: AKModel/models.py:1301 +#: AKModel/models.py:1320 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:1321 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:1322 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:1323 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:1324 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:1326 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:1327 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:1328 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:1329 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:1330 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:1336 msgid "Warning" msgstr "Warnung" -#: AKModel/models.py:1318 +#: AKModel/models.py:1337 msgid "Violation" msgstr "Verletzung" -#: AKModel/models.py:1320 +#: AKModel/models.py:1339 msgid "Type" msgstr "Art" -#: AKModel/models.py:1321 +#: AKModel/models.py:1340 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:1341 msgid "Level" msgstr "Level" -#: AKModel/models.py:1323 +#: AKModel/models.py:1342 msgid "Severity level of the violation" msgstr "Schweregrad der Verletzung" -#: AKModel/models.py:1330 +#: AKModel/models.py:1349 msgid "AK(s) belonging to this constraint" msgstr "AK(s), die zu diesem Constraint gehören" -#: AKModel/models.py:1332 +#: AKModel/models.py:1351 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:1353 msgid "AK Owner belonging to this constraint" msgstr "AK Leitung(en), die zu diesem Constraint gehören" -#: AKModel/models.py:1336 +#: AKModel/models.py:1355 msgid "Room belonging to this constraint" msgstr "Raum, der zu diesem Constraint gehört" -#: AKModel/models.py:1339 +#: AKModel/models.py:1358 msgid "AK Requirement belonging to this constraint" msgstr "AK Anforderung, die zu diesem Constraint gehört" -#: AKModel/models.py:1341 +#: AKModel/models.py:1360 msgid "AK Category belonging to this constraint" msgstr "AK Kategorie, di zu diesem Constraint gehört" -#: AKModel/models.py:1343 +#: AKModel/models.py:1362 msgid "Comment" msgstr "Kommentar" -#: AKModel/models.py:1343 +#: AKModel/models.py:1362 msgid "Comment or further details for this violation" msgstr "Kommentar oder weitere Details zu dieser Vereletzung" -#: AKModel/models.py:1346 +#: AKModel/models.py:1365 msgid "Timestamp" msgstr "Timestamp" -#: AKModel/models.py:1346 +#: AKModel/models.py:1365 msgid "Time of creation" msgstr "Zeitpunkt der ERstellung" -#: AKModel/models.py:1347 +#: AKModel/models.py:1366 msgid "Manually Resolved" msgstr "Manuell behoben" -#: AKModel/models.py:1348 +#: AKModel/models.py:1367 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:1394 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:1529 msgid "Default Slot" msgstr "Standardslot" -#: AKModel/models.py:1514 +#: AKModel/models.py:1534 msgid "Slot End" msgstr "Ende des Slots" -#: AKModel/models.py:1514 +#: AKModel/models.py:1534 msgid "Time and date the slot ends" msgstr "Zeit und Datum zu der der Slot endet" -#: AKModel/models.py:1519 +#: AKModel/models.py:1539 msgid "Primary categories" msgstr "Primäre Kategorien" -#: AKModel/models.py:1520 +#: AKModel/models.py:1541 msgid "Categories that should be assigned to this slot primarily" msgstr "Kategorieren, die diesem Slot primär zugewiesen werden sollen" -#: AKModel/site.py:13 AKModel/site.py:14 +#: AKModel/site.py:14 msgid "Administration" msgstr "Verwaltung" @@ -1241,35 +1241,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" @@ -1455,6 +1455,12 @@ msgstr "Zu Anforderungen gehörige AKs anzeigen" msgid "Event Status" msgstr "Eventstatus" +#~ msgid "Conflicts" +#~ msgstr "Konflikte" + +#~ msgid "Prerequisites" +#~ msgstr "Voraussetzungen" + #~ msgid "Opening time for expression of interest." #~ msgstr "Öffnungszeitpunkt für die Angabe von Interesse an AKs." 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/migrations/0064_merge_20250304_1416.py b/AKModel/migrations/0064_merge_20250304_1416.py new file mode 100644 index 0000000000000000000000000000000000000000..456a64455182a03bd08b5c030bed1284adaccc67 --- /dev/null +++ b/AKModel/migrations/0064_merge_20250304_1416.py @@ -0,0 +1,14 @@ +# Generated by Django 5.1.6 on 2025-03-04 14:16 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('AKModel', '0063_field_validators'), + ('AKModel', '0063_merge_0061_event_export_slot_0062_interest_no_history'), + ] + + operations = [ + ] diff --git a/AKModel/models.py b/AKModel/models.py index e6271580dd69e8167a54f31393dd7e8503d298a3..9d7b6c2e15dc332fec9472a0553878670a7288a2 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -4,10 +4,11 @@ import json import math from dataclasses import dataclass from datetime import datetime, timedelta -from typing import Any, Iterable, Generator +from typing import Any, Generator, Iterable -from django.db import models, transaction from django.apps import apps +from django.core.validators import RegexValidator +from django.db import models, transaction from django.db.models import Count from django.urls import reverse_lazy from django.utils import timezone @@ -16,6 +17,15 @@ from django.utils.translation import gettext_lazy as _ 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: @@ -41,17 +51,18 @@ class OptimizerTimeslot: avail = self.avail.merge_with(other.avail) constraints = self.constraints.union(other.constraints) return OptimizerTimeslot( - avail=avail, idx=self.idx, constraints=constraints + avail=avail, idx=self.idx, constraints=constraints ) def __repr__(self) -> str: return f"({self.avail.simplified}, {self.idx}, {self.constraints})" + TimeslotBlock = list[OptimizerTimeslot] def merge_blocks( - blocks: Iterable[TimeslotBlock] + blocks: Iterable[TimeslotBlock] ) -> Iterable[TimeslotBlock]: """Merge iterable of blocks together. @@ -73,8 +84,8 @@ def merge_blocks( # sort timeslots according to start timeslots = sorted( - timeslot_chain, - key=lambda slot: slot.avail.start + timeslot_chain, + key=lambda slot: slot.avail.start ) if not timeslots: @@ -87,8 +98,8 @@ def merge_blocks( for slot in timeslots: if current_block and slot.avail.overlaps(current_block[-1].avail, strict=True): if ( - slot.avail.start == current_block[-1].avail.start - and slot.avail.end == current_block[-1].avail.end + slot.avail.start == current_block[-1].avail.start + and slot.avail.end == current_block[-1].avail.end ): # the same timeslot -> merge current_block[-1] = current_block[-1].merge(slot) @@ -96,8 +107,8 @@ def merge_blocks( # partial overlap of interiors -> not supported # TODO: Show comprehensive message in production raise ValueError( - "Partially overlapping timeslots are not supported!" - f" ({current_block[-1].avail.simplified}, {slot.avail.simplified})" + "Partially overlapping timeslots are not supported!" + f" ({current_block[-1].avail.simplified}, {slot.avail.simplified})" ) elif not current_block or slot.avail.overlaps(current_block[-1].avail, strict=False): # only endpoints in intersection -> same block @@ -132,8 +143,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,22 +157,21 @@ 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) default_slot = models.DecimalField(max_digits=4, decimal_places=2, default=2, verbose_name=_('Default Slot Length'), help_text=_('Default length in hours that is assumed for AKs in this event.')) export_slot = models.DecimalField(max_digits=4, decimal_places=2, default=1, verbose_name=_('Export Slot Length'), - help_text=_( - 'Slot duration in hours that is used in the timeslot discretization, ' - 'when this event is exported for the solver.' - )) - + help_text=_( + 'Slot duration in hours that is used in the timeslot discretization, ' + 'when this event is exported for the solver.' + )) 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 +202,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, @@ -269,13 +280,13 @@ class Event(models.Model): ) def _generate_slots_from_block( - self, - start: datetime, - end: datetime, - slot_duration: timedelta, - *, - slot_index: int = 0, - constraints: set[str] | None = None, + self, + start: datetime, + end: datetime, + slot_duration: timedelta, + *, + slot_index: int = 0, + constraints: set[str] | None = None, ) -> Generator[TimeslotBlock, None, int]: """Discretize a time range into timeslots. @@ -316,22 +327,22 @@ class Event(models.Model): while current_slot_start + slot_duration <= end: slot = Availability( - event=self, - start=current_slot_start, - end=current_slot_start + slot_duration, + event=self, + start=current_slot_start, + end=current_slot_start + slot_duration, ) if any((availability.contains(slot) for availability in room_availabilities)): # no gap in a block if ( - previous_slot_start is not None - and previous_slot_start + slot_duration < current_slot_start + previous_slot_start is not None + and previous_slot_start + slot_duration < current_slot_start ): yield current_block current_block = [] current_block.append( - OptimizerTimeslot(avail=slot, idx=slot_index, constraints=constraints) + OptimizerTimeslot(avail=slot, idx=slot_index, constraints=constraints) ) previous_slot_start = current_slot_start @@ -355,14 +366,14 @@ class Event(models.Model): :ytype: list of OptimizerTimeslot """ all_category_constraints = AKCategory.create_category_constraints( - AKCategory.objects.filter(event=self).all() + AKCategory.objects.filter(event=self).all() ) yield from self._generate_slots_from_block( - start=self.start, - end=self.end, - slot_duration=timedelta(hours=1.0 / slots_in_an_hour), - constraints=all_category_constraints, + start=self.start, + end=self.end, + slot_duration=timedelta(hours=1.0 / slots_in_an_hour), + constraints=all_category_constraints, ) def default_time_slots(self, *, slots_in_an_hour: float) -> Iterable[TimeslotBlock]: @@ -380,15 +391,15 @@ class Event(models.Model): for block_slot in DefaultSlot.objects.filter(event=self).order_by("start", "end"): category_constraints = AKCategory.create_category_constraints( - block_slot.primary_categories.all() + block_slot.primary_categories.all() ) slot_index = yield from self._generate_slots_from_block( - start=block_slot.start, - end=block_slot.end, - slot_duration=slot_duration, - slot_index=slot_index, - constraints=category_constraints, + start=block_slot.start, + end=block_slot.end, + slot_duration=slot_duration, + slot_index=slot_index, + constraints=category_constraints, ) def discretize_timeslots(self, *, slots_in_an_hour: float | None = None) -> Iterable[TimeslotBlock]: @@ -441,7 +452,7 @@ class Event(models.Model): if not scheduled_slot["timeslot_ids"]: raise ValueError( - _("AK {ak_name} is not assigned any timeslot by the solver").format(ak_name=slot.ak.name) + _("AK {ak_name} is not assigned any timeslot by the solver").format(ak_name=slot.ak.name) ) start_timeslot = timeslot_dict[min(scheduled_slot["timeslot_ids"])].avail @@ -450,39 +461,39 @@ class Event(models.Model): if solver_duration + 2e-4 < slot.duration: raise ValueError( - _( - "Duration of AK {ak_name} assigned by solver ({solver_duration} hours) " - "is less than the duration required by the slot ({slot_duration} hours)" - ).format( - ak_name=slot.ak.name, - solver_duration=solver_duration, - slot_duration=slot.duration, - ) + _( + "Duration of AK {ak_name} assigned by solver ({solver_duration} hours) " + "is less than the duration required by the slot ({slot_duration} hours)" + ).format( + ak_name=slot.ak.name, + solver_duration=solver_duration, + slot_duration=slot.duration, + ) ) if slot.fixed: solver_room = Room.objects.get(id=int(scheduled_slot["room_id"])) if slot.room != solver_room: raise ValueError( - _( - "Fixed AK {ak_name} assigned by solver to room {solver_room} " - "is fixed to room {slot_room}" - ).format( - ak_name=slot.ak.name, - solver_room=solver_room.name, - slot_room=slot.room.name, - ) + _( + "Fixed AK {ak_name} assigned by solver to room {solver_room} " + "is fixed to room {slot_room}" + ).format( + ak_name=slot.ak.name, + solver_room=solver_room.name, + slot_room=slot.room.name, + ) ) if slot.start != start_timeslot.start: raise ValueError( - _( - "Fixed AK {ak_name} assigned by solver to start at {solver_start} " - "is fixed to start at {slot_start}" - ).format( - ak_name=slot.ak.name, - solver_start=start_timeslot.start, - slot_start=slot.start, - ) + _( + "Fixed AK {ak_name} assigned by solver to start at {solver_start} " + "is fixed to start at {slot_start}" + ).format( + ak_name=slot.ak.name, + solver_start=start_timeslot.start, + slot_start=slot.start, + ) ) else: slot.room = Room.objects.get(id=int(scheduled_slot["room_id"])) @@ -520,14 +531,14 @@ class Event(models.Model): def _test_add_constraint(slot: Availability, availabilities: list[Availability]) -> bool: """Test if object is not available for whole event and may happen during slot.""" return ( - _test_event_not_covered(availabilities) and slot.is_covered(availabilities) + _test_event_not_covered(availabilities) and slot.is_covered(availabilities) ) def _generate_time_constraints( - avail_label: str, - avail_dict: dict, - timeslot_avail: Availability, - prefix: str = "availability", + avail_label: str, + avail_dict: dict, + timeslot_avail: Availability, + prefix: str = "availability", ) -> list[str]: return [ f"{prefix}-{avail_label}-{pk}" @@ -538,7 +549,7 @@ class Event(models.Model): timeslots = { "info": {"duration": float(self.export_slot)}, "blocks": [], - } + } rooms = Room.objects.filter(event=self).order_by() slots = AKSlot.objects.filter(event=self).order_by() @@ -589,17 +600,17 @@ class Event(models.Model): # add fulfilled time constraints for all AKs that cannot happen during full event time_constraints.extend( - _generate_time_constraints("ak", ak_availabilities, timeslot.avail) + _generate_time_constraints("ak", ak_availabilities, timeslot.avail) ) # add fulfilled time constraints for all persons that are not available for full event time_constraints.extend( - _generate_time_constraints("person", person_availabilities, timeslot.avail) + _generate_time_constraints("person", person_availabilities, timeslot.avail) ) # add fulfilled time constraints for all rooms that are not available for full event time_constraints.extend( - _generate_time_constraints("room", room_availabilities, timeslot.avail) + _generate_time_constraints("room", room_availabilities, timeslot.avail) ) # add fulfilled time constraints for all AKSlots fixed to happen during timeslot @@ -619,7 +630,7 @@ class Event(models.Model): "end": timeslot.avail.end.astimezone(self.timezone).strftime("%Y-%m-%d %H:%M"), }, "fulfilled_time_constraints": sorted(time_constraints), - }) + }) timeslots["blocks"].append(current_block) @@ -646,7 +657,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 +738,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 +835,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 +852,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 +870,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 +1012,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 +1021,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 +1177,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 +1185,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. @@ -1217,8 +1234,8 @@ class AKSlot(models.Model): "properties": { "conflicts": sorted( - [conflict.pk for conflict in conflict_slots.all()] - + [second_slot.pk for second_slot in other_ak_slots.all()] + [conflict.pk for conflict in conflict_slots.all()] + + [second_slot.pk for second_slot in other_ak_slots.all()] ), "dependencies": sorted([dep.pk for dep in dependency_slots.all()]), }, @@ -1234,8 +1251,8 @@ class AKSlot(models.Model): "duration_in_hours": float(self.duration), "django_ak_id": self.ak.pk, "types": list(self.ak.types.values_list("name", flat=True).order_by()), - }, - } + }, + } data["time_constraints"].extend(ak_time_constraints) for owner in self.ak.owners.all(): @@ -1256,6 +1273,7 @@ class AKSlot(models.Model): return data + class AKOrgaMessage(models.Model): """ Model representing confidential messages to the organizers/scheduling people, belonging to a certain AK @@ -1288,6 +1306,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 +1323,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 +1524,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 +1537,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/AKScheduling/locale/de_DE/LC_MESSAGES/django.po b/AKScheduling/locale/de_DE/LC_MESSAGES/django.po index a2fb73ef747f3bc8e7aef7e38ddaef1d28275acd..3944e8722fe004177ef48f6119fa1bd5dd6e38d3 100644 --- a/AKScheduling/locale/de_DE/LC_MESSAGES/django.po +++ b/AKScheduling/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 14:49+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" @@ -107,7 +107,6 @@ msgid "Event Status" msgstr "Event-Status" #: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:113 -#: AKScheduling/views.py:50 msgid "Scheduling" msgstr "Scheduling" @@ -240,7 +239,6 @@ msgstr[1] "" " " #: AKScheduling/templates/admin/AKScheduling/unscheduled.html:7 -#: AKScheduling/views.py:25 msgid "Unscheduled AK Slots" msgstr "Noch nicht geschedulte AK-Slots" @@ -248,22 +246,10 @@ msgstr "Noch nicht geschedulte AK-Slots" msgid "Count" msgstr "Anzahl" -#: AKScheduling/views.py:91 -msgid "Constraint violations for" -msgstr "Constraintverletzungen für" - -#: AKScheduling/views.py:106 -msgid "AKs requiring special attention for" -msgstr "AKs die besondere Aufmerksamkeit erfordern für" - #: AKScheduling/views.py:152 msgid "Interest updated" msgstr "Interesse aktualisiert" -#: AKScheduling/views.py:166 -msgid "Enter interest" -msgstr "Interesse eingeben" - #: AKScheduling/views.py:210 msgid "Wishes" msgstr "Wünsche" @@ -319,6 +305,15 @@ msgstr "Standardverfügbarkeiten für {count} AKs angelegt" msgid "Constraint Violations" msgstr "Constraintverletzungen" +#~ msgid "Constraint violations for" +#~ msgstr "Constraintverletzungen für" + +#~ msgid "AKs requiring special attention for" +#~ msgstr "AKs die besondere Aufmerksamkeit erfordern für" + +#~ msgid "Enter interest" +#~ msgstr "Interesse eingeben" + #~ msgid "Bitte AK auswählen" #~ msgstr "Please sel" 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..1ccfaeb613e93c43db06c116b542e49efca85658 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 14:49+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" @@ -109,21 +109,25 @@ msgstr "AK-Wunsch" #: AKSubmission/templates/AKSubmission/ak_detail.html:186 #, python-format msgid "" -"This AK currently takes place for another <span v-html=\"timeUntilEnd\">" -"%(featured_slot_remaining)s</span> minute(s) in %(room)s. " +"This AK currently takes place for another <span v-" +"html=\"timeUntilEnd\">%(featured_slot_remaining)s</span> minute(s) in " +"%(room)s. " msgstr "" -"Dieser AK findet noch <span v-html=\"timeUntilEnd\">" -"%(featured_slot_remaining)s</span> Minute(n) in %(room)s statt. \n" +"Dieser AK findet noch <span v-" +"html=\"timeUntilEnd\">%(featured_slot_remaining)s</span> Minute(n) in " +"%(room)s statt. \n" " " #: AKSubmission/templates/AKSubmission/ak_detail.html:189 #, python-format msgid "" -"This AK starts in <span v-html=\"timeUntilStart\">" -"%(featured_slot_remaining)s</span> minute(s) in %(room)s. " +"This AK starts in <span v-" +"html=\"timeUntilStart\">%(featured_slot_remaining)s</span> minute(s) in " +"%(room)s. " msgstr "" -"Dieser AK beginnt in <span v-html=\"timeUntilStart\">" -"%(featured_slot_remaining)s</span> Minute(n) in %(room)s. \n" +"Dieser AK beginnt in <span v-" +"html=\"timeUntilStart\">%(featured_slot_remaining)s</span> Minute(n) in " +"%(room)s. \n" " " #: AKSubmission/templates/AKSubmission/ak_detail.html:194 @@ -274,7 +278,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,81 +404,80 @@ 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 -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" +#~ msgid "AKs with Track" +#~ msgstr "AK mit Track" + #~ msgid "" #~ "Due to technical reasons, the link you entered was truncated to a length " #~ "of 200 characters" 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..4c98ce7b4bdbfdbc21ac3429199ff4a5786e1294 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -10,7 +10,7 @@ setup. ### System Requirements -* Python 3.10+ incl. development tools +* Python 3.11+ incl. development tools * Virtualenv * pdflatex & beamer class (`texlive-latex-base texlive-latex-recommended texlive-latex-extra texlive-fonts-extra texlive-luatex`) @@ -37,7 +37,7 @@ Python requirements are listed in ``requirements.txt``. They can be installed wi ### Manual Setup -1. setup a virtual environment using the proper python version ``virtualenv venv -p python3.10`` +1. setup a virtual environment using the proper python version ``virtualenv venv -p python3.11`` 1. activate virtualenv ``source venv/bin/activate`` 1. install python requirements ``pip install -r requirements.txt`` 1. setup necessary database tables etc. ``python manage.py migrate`` @@ -68,7 +68,7 @@ is not stored in any repository or similar, and disable DEBUG mode (``settings.p 1. create a folder, e.g. ``mkdir /srv/AKPlanning/`` 1. change to the new directory ``cd /srv/AKPlanning/`` 1. clone this repository ``git clone URL .`` -1. setup a virtual environment using the proper python version ``virtualenv venv -p python3.10`` +1. setup a virtual environment using the proper python version ``virtualenv venv -p python3.11`` 1. activate virtualenv ``source venv/bin/activate`` 1. update tools ``pip install --upgrade setuptools pip wheel`` 1. install python requirements ``pip install -r requirements.txt`` diff --git a/Utils/check.sh b/Utils/check.sh index c2e09f5b8852b9a2e70b9c9f6f9e1296c1404f02..1cdbdf7d85687b820c0a16f98217d367ac8b3ccf 100755 --- a/Utils/check.sh +++ b/Utils/check.sh @@ -8,14 +8,20 @@ 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 + 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/Utils/update.sh b/Utils/update.sh index a711b73b912fcd07e8543b6b49d4eb8950ff0d79..d81aae52947404ac629050ba2c2d69c19e4b37e5 100755 --- a/Utils/update.sh +++ b/Utils/update.sh @@ -19,7 +19,7 @@ fi mkdir -p backups/ python manage.py dumpdata --indent=2 > "backups/$(date +"%Y%m%d%H%M")_datadump.json" --traceback -git pull +# git pull pip install --upgrade setuptools pip wheel pip install --upgrade -r requirements.txt diff --git a/requirements.txt b/requirements.txt index 451feef4db2ee295a38687bc0f96ff1dee637418..be46f86ad6caa490c27ac703e3de38154a542b6b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,26 +1,31 @@ -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 + +# Tests +beautifulsoup4==4.13.3 +lxml==5.3.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