diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index fe4d4446e760b1ef6a270759725ffa6ba2cd7c5e..55ef59013fcb8d31491024c17bbbfb9a707653b4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: python:3.9 +image: python:3.10 services: - mysql diff --git a/AKModel/availability/models.py b/AKModel/availability/models.py index 7ce794dcda52fcbde462584379e1447ad2b124f2..35814ee06d2bf49fe9416710c71cbebbb4fc7bb4 100644 --- a/AKModel/availability/models.py +++ b/AKModel/availability/models.py @@ -151,9 +151,12 @@ class Availability(models.Model): if not other.overlaps(self, strict=False): raise Exception('Only overlapping Availabilities can be merged.') - return Availability( + avail = Availability( start=min(self.start, other.start), end=max(self.end, other.end) ) + if self.event == other.event: + avail.event = self.event + return avail def __or__(self, other: 'Availability') -> 'Availability': """Performs the merge operation: ``availability1 | availability2``""" @@ -168,9 +171,12 @@ class Availability(models.Model): if not other.overlaps(self, False): raise Exception('Only overlapping Availabilities can be intersected.') - return Availability( + avail = Availability( start=max(self.start, other.start), end=min(self.end, other.end) ) + if self.event == other.event: + avail.event = self.event + return avail def __and__(self, other: 'Availability') -> 'Availability': """Performs the intersect operation: ``availability1 & @@ -247,7 +253,14 @@ class Availability(models.Model): f'{self.end.astimezone(self.event.timezone).strftime("%a %H:%M")}') @classmethod - def with_event_length(cls, event, person=None, room=None, ak=None, ak_category=None): + def with_event_length( + cls, + event: Event, + person: AKOwner | None = None, + room: Room | None = None, + ak: AK | None = None, + ak_category: AKCategory | None = None, + ) -> "Availability": """ Create an availability covering exactly the time between event start and event end. Can e.g., be used to create default availabilities. @@ -267,6 +280,21 @@ class Availability(models.Model): return Availability(start=timeframe_start, end=timeframe_end, event=event, person=person, room=room, ak=ak, ak_category=ak_category) + @classmethod + def is_event_covered(cls, event: Event, availabilities: List['Availability']) -> bool: + """Check if list of availibilities cover whole event. + + :param event: event to check. + :param availabilities: availabilities to check. + :return: whether the availabilities cover full event. + :rtype: bool + """ + # NOTE: Cannot use `Availability.with_event_length` as its end is the + # event end + 1 day + full_event = Availability(event=event, start=event.start, end=event.end) + avail_union = Availability.union(availabilities) + return not avail_union or avail_union[0].contains(full_event) + class Meta: verbose_name = _('Availability') verbose_name_plural = _('Availabilities') diff --git a/AKModel/forms.py b/AKModel/forms.py index 4d1fe7ef7bfa45d41b377fa4c9815e951e35cf19..74ca1b6813f0365d1179166da18aeb8a8c59ca4e 100644 --- a/AKModel/forms.py +++ b/AKModel/forms.py @@ -272,3 +272,13 @@ class RoomFormWithAvailabilities(AvailabilitiesFormMixin, RoomForm): # Filter possible values for m2m when event is specified if hasattr(self.instance, "event") and self.instance.event is not None: self.fields["properties"].queryset = AKRequirement.objects.filter(event=self.instance.event) + + +class JSONImportForm(AdminIntermediateForm): + """Form to import an AK schedule from a json file.""" + json_data = forms.CharField( + required=True, + widget=forms.Textarea, + label=_("JSON data"), + help_text=_("JSON data from the scheduling solver"), + ) diff --git a/AKModel/locale/de_DE/LC_MESSAGES/django.po b/AKModel/locale/de_DE/LC_MESSAGES/django.po index 261c5b2c06e56ee965f4d11f895e00b83f017cbd..4cc752ae7d4ac4da883547ad18a5fbe866d816d7 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: 2024-04-25 01:29+0200\n" +"POT-Creation-Date: 2024-05-31 14:22+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" @@ -25,18 +25,17 @@ msgstr "Status" msgid "Toggle plan visibility" msgstr "Plansichtbarkeit ändern" -#: AKModel/admin.py:110 AKModel/admin.py:121 AKModel/views/manage.py:138 +#: AKModel/admin.py:110 AKModel/admin.py:121 AKModel/views/manage.py:140 msgid "Publish plan" msgstr "Plan veröffentlichen" -#: AKModel/admin.py:113 AKModel/admin.py:129 AKModel/views/manage.py:151 +#: AKModel/admin.py:113 AKModel/admin.py:129 AKModel/views/manage.py:153 msgid "Unpublish plan" msgstr "Plan verbergen" -#: AKModel/admin.py:168 AKModel/models.py:360 AKModel/models.py:682 -#: AKModel/templates/admin/AKModel/aks_by_user.html:12 +#: AKModel/admin.py:168 AKModel/models.py:612 AKModel/models.py:1028 #: AKModel/templates/admin/AKModel/status/event_aks.html:10 -#: AKModel/views/manage.py:73 AKModel/views/status.py:98 +#: AKModel/views/manage.py:75 AKModel/views/status.py:97 msgid "AKs" msgstr "AKs" @@ -60,11 +59,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:320 AKModel/views/ak.py:99 +#: AKModel/admin.py:320 AKModel/views/ak.py:226 msgid "Reset interest in AKs" msgstr "Interesse an AKs zurücksetzen" -#: AKModel/admin.py:330 AKModel/views/ak.py:114 +#: AKModel/admin.py:330 AKModel/views/ak.py:241 msgid "Reset AKs' interest counters" msgstr "Interessenszähler der AKs zurücksetzen" @@ -72,19 +71,19 @@ msgstr "Interessenszähler der AKs zurücksetzen" msgid "AK Details" msgstr "AK-Details" -#: AKModel/admin.py:505 AKModel/views/manage.py:99 +#: AKModel/admin.py:505 AKModel/views/manage.py:101 msgid "Mark Constraint Violations as manually resolved" msgstr "Markiere Constraintverletzungen als manuell behoben" -#: AKModel/admin.py:514 AKModel/views/manage.py:112 +#: AKModel/admin.py:514 AKModel/views/manage.py:114 msgid "Set Constraint Violations to level \"violation\"" msgstr "Constraintverletzungen auf Level \"Violation\" setzen" -#: AKModel/admin.py:523 AKModel/views/manage.py:125 +#: AKModel/admin.py:523 AKModel/views/manage.py:127 msgid "Set Constraint Violations to level \"warning\"" msgstr "Constraintverletzungen auf Level \"Warning\" setzen" -#: AKModel/availability/forms.py:25 AKModel/availability/models.py:271 +#: AKModel/availability/forms.py:25 AKModel/availability/models.py:299 msgid "Availability" msgstr "Verfügbarkeit" @@ -109,17 +108,17 @@ msgstr "Die eingegebene Verfügbarkeit enthält ein ungültiges Datum." msgid "Please fill in your availabilities!" msgstr "Bitte Verfügbarkeiten eintragen!" -#: AKModel/availability/models.py:43 AKModel/models.py:60 AKModel/models.py:174 -#: AKModel/models.py:251 AKModel/models.py:270 AKModel/models.py:296 -#: AKModel/models.py:350 AKModel/models.py:492 AKModel/models.py:531 -#: AKModel/models.py:621 AKModel/models.py:678 AKModel/models.py:869 +#: AKModel/availability/models.py:43 AKModel/models.py:91 AKModel/models.py:412 +#: AKModel/models.py:489 AKModel/models.py:522 AKModel/models.py:548 +#: AKModel/models.py:602 AKModel/models.py:744 AKModel/models.py:820 +#: AKModel/models.py:967 AKModel/models.py:1024 AKModel/models.py:1215 msgid "Event" msgstr "Event" -#: AKModel/availability/models.py:44 AKModel/models.py:175 -#: AKModel/models.py:252 AKModel/models.py:271 AKModel/models.py:297 -#: AKModel/models.py:351 AKModel/models.py:493 AKModel/models.py:532 -#: AKModel/models.py:622 AKModel/models.py:679 AKModel/models.py:870 +#: AKModel/availability/models.py:44 AKModel/models.py:413 +#: AKModel/models.py:490 AKModel/models.py:523 AKModel/models.py:549 +#: AKModel/models.py:603 AKModel/models.py:745 AKModel/models.py:821 +#: AKModel/models.py:968 AKModel/models.py:1025 AKModel/models.py:1216 msgid "Associated event" msgstr "Zugehöriges Event" @@ -131,8 +130,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:496 -#: AKModel/models.py:521 AKModel/models.py:688 +#: AKModel/availability/models.py:61 AKModel/models.py:748 +#: AKModel/models.py:810 AKModel/models.py:1034 msgid "Room" msgstr "Raum" @@ -140,8 +139,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:359 -#: AKModel/models.py:520 AKModel/models.py:616 +#: AKModel/availability/models.py:70 AKModel/models.py:611 +#: AKModel/models.py:809 AKModel/models.py:962 msgid "AK" msgstr "AK" @@ -149,8 +148,8 @@ msgstr "AK" msgid "AK whose availability this is" msgstr "Verfügbarkeiten" -#: AKModel/availability/models.py:79 AKModel/models.py:255 -#: AKModel/models.py:694 +#: AKModel/availability/models.py:79 AKModel/models.py:493 +#: AKModel/models.py:1040 msgid "AK Category" msgstr "AK-Kategorie" @@ -158,7 +157,7 @@ msgstr "AK-Kategorie" msgid "AK Category whose availability this is" msgstr "AK-Kategorie, deren Verfügbarkeit hier abgebildet wird" -#: AKModel/availability/models.py:272 +#: AKModel/availability/models.py:300 msgid "Availabilities" msgstr "Verfügbarkeiten" @@ -220,7 +219,7 @@ msgstr "" "fürWünsche markieren, z.B. um während der Präsentation auf einem Touchscreen " "ausgefüllt zu werden?" -#: AKModel/forms.py:189 AKModel/models.py:863 +#: AKModel/forms.py:189 AKModel/models.py:1209 msgid "Default Slots" msgstr "Standardslots" @@ -259,7 +258,15 @@ msgstr "Standardverfügbarkeiten für alle Räume anlegen?" msgid "CSV must contain a name column" msgstr "CSV muss eine name-Spalte enthalten" -#: AKModel/metaviews/admin.py:156 AKModel/models.py:29 +#: AKModel/forms.py:282 +msgid "JSON data" +msgstr "JSON-Daten" + +#: AKModel/forms.py:283 +msgid "JSON data from the scheduling solver" +msgstr "JSON-Daten, die der scheduling-solver produziert hat" + +#: AKModel/metaviews/admin.py:156 AKModel/models.py:60 msgid "Start" msgstr "Start" @@ -284,66 +291,66 @@ msgstr "Aktivieren?" msgid "Finish" msgstr "Abschluss" -#: AKModel/models.py:20 AKModel/models.py:243 AKModel/models.py:267 -#: AKModel/models.py:294 AKModel/models.py:312 AKModel/models.py:484 +#: AKModel/models.py:51 AKModel/models.py:481 AKModel/models.py:519 +#: AKModel/models.py:546 AKModel/models.py:564 AKModel/models.py:736 msgid "Name" msgstr "Name" -#: AKModel/models.py:21 +#: AKModel/models.py:52 msgid "Name or iteration of the event" msgstr "Name oder Iteration des Events" -#: AKModel/models.py:22 +#: AKModel/models.py:53 msgid "Short Form" msgstr "Kurzer Name" -#: AKModel/models.py:23 +#: AKModel/models.py:54 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:25 +#: AKModel/models.py:56 msgid "Place" msgstr "Ort" -#: AKModel/models.py:26 +#: AKModel/models.py:57 msgid "City etc. the event takes place in" msgstr "Stadt o.ä. in der das Event stattfindet" -#: AKModel/models.py:28 +#: AKModel/models.py:59 msgid "Time Zone" msgstr "Zeitzone" -#: AKModel/models.py:28 +#: AKModel/models.py:59 msgid "Time Zone where this event takes place in" msgstr "Zeitzone in der das Event stattfindet" -#: AKModel/models.py:29 +#: AKModel/models.py:60 msgid "Time the event begins" msgstr "Zeit zu der das Event beginnt" -#: AKModel/models.py:30 +#: AKModel/models.py:61 msgid "End" msgstr "Ende" -#: AKModel/models.py:30 +#: AKModel/models.py:61 msgid "Time the event ends" msgstr "Zeit zu der das Event endet" -#: AKModel/models.py:31 +#: AKModel/models.py:62 msgid "Resolution Deadline" msgstr "Resolutionsdeadline" -#: AKModel/models.py:32 +#: AKModel/models.py:63 msgid "When should AKs with intention to submit a resolution be done?" msgstr "Wann sollen AKs mit Resolutionsabsicht stattgefunden haben?" -#: AKModel/models.py:34 +#: AKModel/models.py:65 msgid "Interest Window Start" msgstr "Beginn Interessensbekundung" -#: AKModel/models.py:36 +#: AKModel/models.py:67 msgid "" "Opening time for expression of interest. When left blank, no interest " "indication will be possible." @@ -351,71 +358,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:38 +#: AKModel/models.py:69 msgid "Interest Window End" msgstr "Ende Interessensbekundung" -#: AKModel/models.py:39 +#: AKModel/models.py:70 msgid "Closing time for expression of interest." msgstr "Öffnungszeitpunkt für die Angabe von Interesse an AKs." -#: AKModel/models.py:41 +#: AKModel/models.py:72 msgid "Public event" msgstr "Öffentliches Event" -#: AKModel/models.py:42 +#: AKModel/models.py:73 msgid "Show this event on overview page." msgstr "Zeige dieses Event auf der Übersichtseite an" -#: AKModel/models.py:44 +#: AKModel/models.py:75 msgid "Active State" msgstr "Aktiver Status" -#: AKModel/models.py:44 +#: AKModel/models.py:75 msgid "Marks currently active events" msgstr "Markiert aktuell aktive Events" -#: AKModel/models.py:45 +#: AKModel/models.py:76 msgid "Plan Hidden" msgstr "Plan verborgen" -#: AKModel/models.py:45 +#: AKModel/models.py:76 msgid "Hides plan for non-staff users" msgstr "Verbirgt den Plan für Nutzer*innen ohne erweiterte Rechte" -#: AKModel/models.py:47 +#: AKModel/models.py:78 msgid "Plan published at" msgstr "Plan veröffentlicht am/um" -#: AKModel/models.py:48 +#: AKModel/models.py:79 msgid "Timestamp at which the plan was published" msgstr "Zeitpunkt, zu dem der Plan veröffentlicht wurde" -#: AKModel/models.py:50 +#: AKModel/models.py:81 msgid "Base URL" msgstr "URL-Prefix" -#: AKModel/models.py:50 +#: AKModel/models.py:81 msgid "Prefix for wiki link construction" msgstr "Prefix für die automatische Generierung von Wiki-Links" -#: AKModel/models.py:51 +#: AKModel/models.py:82 msgid "Wiki Export Template Name" msgstr "Wiki-Export Templatename" -#: AKModel/models.py:52 +#: AKModel/models.py:83 msgid "Default Slot Length" msgstr "Standardslotlänge" -#: AKModel/models.py:53 +#: AKModel/models.py:84 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:55 +#: AKModel/models.py:86 msgid "Contact email address" msgstr "E-Mail Kontaktadresse" -#: AKModel/models.py:56 +#: AKModel/models.py:87 msgid "" "An email address that is displayed on every page and can be used for all " "kinds of questions" @@ -423,75 +430,75 @@ msgstr "" "Eine Mailadresse die auf jeder Seite angezeigt wird und für alle Arten von " "Fragen genutzt werden kann" -#: AKModel/models.py:61 +#: AKModel/models.py:92 msgid "Events" msgstr "Events" -#: AKModel/models.py:169 +#: AKModel/models.py:407 msgid "Nickname" msgstr "Spitzname" -#: AKModel/models.py:169 +#: AKModel/models.py:407 msgid "Name to identify an AK owner by" msgstr "Name, durch den eine AK-Leitung identifiziert wird" -#: AKModel/models.py:170 +#: AKModel/models.py:408 msgid "Slug" msgstr "Slug" -#: AKModel/models.py:170 +#: AKModel/models.py:408 msgid "Slug for URL generation" msgstr "Slug für URL-Generierung" -#: AKModel/models.py:171 +#: AKModel/models.py:409 msgid "Institution" msgstr "Instutution" -#: AKModel/models.py:171 +#: AKModel/models.py:409 msgid "Uni etc." msgstr "Universität o.ä." -#: AKModel/models.py:172 AKModel/models.py:321 +#: AKModel/models.py:410 AKModel/models.py:573 msgid "Web Link" msgstr "Internet Link" -#: AKModel/models.py:172 +#: AKModel/models.py:410 msgid "Link to Homepage" msgstr "Link zu Homepage oder Webseite" -#: AKModel/models.py:178 AKModel/models.py:687 +#: AKModel/models.py:416 AKModel/models.py:1033 msgid "AK Owner" msgstr "AK-Leitung" -#: AKModel/models.py:179 +#: AKModel/models.py:417 msgid "AK Owners" msgstr "AK-Leitungen" -#: AKModel/models.py:243 +#: AKModel/models.py:481 msgid "Name of the AK Category" msgstr "Name der AK-Kategorie" -#: AKModel/models.py:244 AKModel/models.py:268 +#: AKModel/models.py:482 AKModel/models.py:520 msgid "Color" msgstr "Farbe" -#: AKModel/models.py:244 AKModel/models.py:268 +#: AKModel/models.py:482 AKModel/models.py:520 msgid "Color for displaying" msgstr "Farbe für die Anzeige" -#: AKModel/models.py:245 AKModel/models.py:315 +#: AKModel/models.py:483 AKModel/models.py:567 msgid "Description" msgstr "Beschreibung" -#: AKModel/models.py:246 +#: AKModel/models.py:484 msgid "Short description of this AK Category" msgstr "Beschreibung der AK-Kategorie" -#: AKModel/models.py:247 +#: AKModel/models.py:485 msgid "Present by default" msgstr "Defaultmäßig präsentieren" -#: AKModel/models.py:248 +#: AKModel/models.py:486 msgid "" "Present AKs of this category by default if AK owner did not specify whether " "this AK should be presented?" @@ -499,132 +506,132 @@ msgstr "" "AKs dieser Kategorie standardmäßig vorstellen, wenn die Leitungen das für " "ihren AK nicht explizit spezifiziert haben?" -#: AKModel/models.py:256 +#: AKModel/models.py:494 msgid "AK Categories" msgstr "AK-Kategorien" -#: AKModel/models.py:267 +#: AKModel/models.py:519 msgid "Name of the AK Track" msgstr "Name des AK-Tracks" -#: AKModel/models.py:274 +#: AKModel/models.py:526 msgid "AK Track" msgstr "AK-Track" -#: AKModel/models.py:275 +#: AKModel/models.py:527 msgid "AK Tracks" msgstr "AK-Tracks" -#: AKModel/models.py:294 +#: AKModel/models.py:546 msgid "Name of the Requirement" msgstr "Name der Anforderung" -#: AKModel/models.py:300 AKModel/models.py:691 +#: AKModel/models.py:552 AKModel/models.py:1037 msgid "AK Requirement" msgstr "AK-Anforderung" -#: AKModel/models.py:301 +#: AKModel/models.py:553 msgid "AK Requirements" msgstr "AK-Anforderungen" -#: AKModel/models.py:312 +#: AKModel/models.py:564 msgid "Name of the AK" msgstr "Name des AKs" -#: AKModel/models.py:313 +#: AKModel/models.py:565 msgid "Short Name" msgstr "Kurzer Name" -#: AKModel/models.py:314 +#: AKModel/models.py:566 msgid "Name displayed in the schedule" msgstr "Name zur Anzeige im AK-Plan" -#: AKModel/models.py:315 +#: AKModel/models.py:567 msgid "Description of the AK" msgstr "Beschreibung des AKs" -#: AKModel/models.py:317 +#: AKModel/models.py:569 msgid "Owners" msgstr "Leitungen" -#: AKModel/models.py:318 +#: AKModel/models.py:570 msgid "Those organizing the AK" msgstr "Menschen, die den AK organisieren und halten" -#: AKModel/models.py:321 +#: AKModel/models.py:573 msgid "Link to wiki page" msgstr "Link zur Wiki Seite" -#: AKModel/models.py:322 +#: AKModel/models.py:574 msgid "Protocol Link" msgstr "Protokolllink" -#: AKModel/models.py:322 +#: AKModel/models.py:574 msgid "Link to protocol" msgstr "Link zum Protokoll" -#: AKModel/models.py:324 +#: AKModel/models.py:576 msgid "Category" msgstr "Kategorie" -#: AKModel/models.py:325 +#: AKModel/models.py:577 msgid "Category of the AK" msgstr "Kategorie des AKs" -#: AKModel/models.py:326 +#: AKModel/models.py:578 msgid "Track" msgstr "Track" -#: AKModel/models.py:327 +#: AKModel/models.py:579 msgid "Track the AK belongs to" msgstr "Track zu dem der AK gehört" -#: AKModel/models.py:329 +#: AKModel/models.py:581 msgid "Resolution Intention" msgstr "Resolutionsabsicht" -#: AKModel/models.py:330 +#: AKModel/models.py:582 msgid "Intends to submit a resolution" msgstr "Beabsichtigt eine Resolution einzureichen" -#: AKModel/models.py:331 +#: AKModel/models.py:583 msgid "Present this AK" msgstr "AK präsentieren" -#: AKModel/models.py:332 +#: AKModel/models.py:584 msgid "Present results of this AK" msgstr "Die Ergebnisse dieses AKs vorstellen" -#: AKModel/models.py:334 AKModel/views/status.py:163 +#: AKModel/models.py:586 AKModel/views/status.py:170 msgid "Requirements" msgstr "Anforderungen" -#: AKModel/models.py:335 +#: AKModel/models.py:587 msgid "AK's Requirements" msgstr "Anforderungen des AKs" -#: AKModel/models.py:337 +#: AKModel/models.py:589 msgid "Conflicting AKs" msgstr "AK-Konflikte" -#: AKModel/models.py:338 +#: AKModel/models.py:590 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:339 +#: AKModel/models.py:591 msgid "Prerequisite AKs" msgstr "Vorausgesetzte AKs" -#: AKModel/models.py:340 +#: AKModel/models.py:592 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:342 +#: AKModel/models.py:594 msgid "Organizational Notes" msgstr "Notizen zur Organisation" -#: AKModel/models.py:343 +#: AKModel/models.py:595 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/" @@ -634,289 +641,291 @@ msgstr "" "Anmerkungen bitte den Button für Direktnachrichten verwenden (nach dem " "Anlegen/Bearbeiten)." -#: AKModel/models.py:346 +#: AKModel/models.py:598 msgid "Interest" msgstr "Interesse" -#: AKModel/models.py:346 +#: AKModel/models.py:598 msgid "Expected number of people" msgstr "Erwartete Personenzahl" -#: AKModel/models.py:347 +#: AKModel/models.py:599 msgid "Interest Counter" msgstr "Interessenszähler" -#: AKModel/models.py:348 +#: AKModel/models.py:600 msgid "People who have indicated interest online" msgstr "Anzahl Personen, die online Interesse bekundet haben" -#: AKModel/models.py:353 +#: AKModel/models.py:605 msgid "Export?" msgstr "Export?" -#: AKModel/models.py:354 +#: AKModel/models.py:606 msgid "Include AK in wiki export?" msgstr "AK bei Wiki-Export berücksichtigen?" -#: AKModel/models.py:484 +#: AKModel/models.py:736 msgid "Name or number of the room" msgstr "Name oder Nummer des Raums" -#: AKModel/models.py:485 +#: AKModel/models.py:737 msgid "Location" msgstr "Ort" -#: AKModel/models.py:486 +#: AKModel/models.py:738 msgid "Name or number of the location" msgstr "Name oder Nummer des Ortes" -#: AKModel/models.py:487 +#: AKModel/models.py:739 msgid "Capacity" msgstr "Kapazität" -#: AKModel/models.py:488 +#: AKModel/models.py:740 msgid "Maximum number of people (-1 for unlimited)." msgstr "Maximale Personenzahl (-1 wenn unbeschränkt)." -#: AKModel/models.py:489 +#: AKModel/models.py:741 msgid "Properties" msgstr "Eigenschaften" -#: AKModel/models.py:490 +#: AKModel/models.py:742 msgid "AK requirements fulfilled by the room" msgstr "AK-Anforderungen, die dieser Raum erfüllt" -#: AKModel/models.py:497 AKModel/views/status.py:60 +#: AKModel/models.py:749 AKModel/views/status.py:59 msgid "Rooms" msgstr "Räume" -#: AKModel/models.py:520 +#: AKModel/models.py:809 msgid "AK being mapped" msgstr "AK, der zugeordnet wird" -#: AKModel/models.py:522 +#: AKModel/models.py:811 msgid "Room the AK will take place in" msgstr "Raum in dem der AK stattfindet" -#: AKModel/models.py:523 AKModel/models.py:866 +#: AKModel/models.py:812 AKModel/models.py:1212 msgid "Slot Begin" msgstr "Beginn des Slots" -#: AKModel/models.py:523 AKModel/models.py:866 +#: AKModel/models.py:812 AKModel/models.py:1212 msgid "Time and date the slot begins" msgstr "Zeit und Datum zu der der AK beginnt" -#: AKModel/models.py:525 +#: AKModel/models.py:814 msgid "Duration" msgstr "Dauer" -#: AKModel/models.py:526 +#: AKModel/models.py:815 msgid "Length in hours" msgstr "Länge in Stunden" -#: AKModel/models.py:528 +#: AKModel/models.py:817 msgid "Scheduling fixed" msgstr "Planung fix" -#: AKModel/models.py:529 +#: AKModel/models.py:818 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:534 +#: AKModel/models.py:823 msgid "Last update" msgstr "Letzte Aktualisierung" -#: AKModel/models.py:537 +#: AKModel/models.py:826 msgid "AK Slot" msgstr "AK-Slot" -#: AKModel/models.py:538 AKModel/models.py:684 +#: AKModel/models.py:827 AKModel/models.py:1030 msgid "AK Slots" msgstr "AK-Slot" -#: AKModel/models.py:560 AKModel/models.py:569 +#: AKModel/models.py:849 AKModel/models.py:858 msgid "Not scheduled yet" msgstr "Noch nicht geplant" -#: AKModel/models.py:617 +#: AKModel/models.py:963 msgid "AK this message belongs to" msgstr "AK zu dem die Nachricht gehört" -#: AKModel/models.py:618 +#: AKModel/models.py:964 msgid "Message text" msgstr "Nachrichtentext" -#: AKModel/models.py:619 +#: AKModel/models.py:965 msgid "Message to the organizers. This is not publicly visible." msgstr "" "Nachricht an die Organisator*innen. Diese ist nicht öffentlich sichtbar." -#: AKModel/models.py:623 +#: AKModel/models.py:969 msgid "Resolved" msgstr "Erledigt" -#: AKModel/models.py:624 +#: AKModel/models.py:970 msgid "This message has been resolved (no further action needed)" -msgstr "Diese Nachricht wurde vollständig bearbeitet (keine weiteren Aktionen notwendig)" +msgstr "" +"Diese Nachricht wurde vollständig bearbeitet (keine weiteren Aktionen " +"notwendig)" -#: AKModel/models.py:627 +#: AKModel/models.py:973 msgid "AK Orga Message" msgstr "AK-Organachricht" -#: AKModel/models.py:628 +#: AKModel/models.py:974 msgid "AK Orga Messages" msgstr "AK-Organachrichten" -#: AKModel/models.py:645 +#: AKModel/models.py:991 msgid "Constraint Violation" msgstr "Constraintverletzung" -#: AKModel/models.py:646 +#: AKModel/models.py:992 msgid "Constraint Violations" msgstr "Constraintverletzungen" -#: AKModel/models.py:653 +#: AKModel/models.py:999 msgid "Owner has two parallel slots" msgstr "Leitung hat zwei Slots parallel" -#: AKModel/models.py:654 +#: AKModel/models.py:1000 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:655 +#: AKModel/models.py:1001 msgid "Room has two AK slots scheduled at the same time" msgstr "Raum hat zwei AK Slots gleichzeitig" -#: AKModel/models.py:656 +#: AKModel/models.py:1002 msgid "Room does not satisfy the requirement of the scheduled AK" msgstr "Room erfüllt die Anforderungen des platzierten AKs nicht" -#: AKModel/models.py:657 +#: AKModel/models.py:1003 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:658 +#: AKModel/models.py:1004 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:660 +#: AKModel/models.py:1006 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:661 +#: AKModel/models.py:1007 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:662 +#: AKModel/models.py:1008 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:663 +#: AKModel/models.py:1009 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:664 +#: AKModel/models.py:1010 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:670 +#: AKModel/models.py:1016 msgid "Warning" msgstr "Warnung" -#: AKModel/models.py:671 +#: AKModel/models.py:1017 msgid "Violation" msgstr "Verletzung" -#: AKModel/models.py:673 +#: AKModel/models.py:1019 msgid "Type" msgstr "Art" -#: AKModel/models.py:674 +#: AKModel/models.py:1020 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:675 +#: AKModel/models.py:1021 msgid "Level" msgstr "Level" -#: AKModel/models.py:676 +#: AKModel/models.py:1022 msgid "Severity level of the violation" msgstr "Schweregrad der Verletzung" -#: AKModel/models.py:683 +#: AKModel/models.py:1029 msgid "AK(s) belonging to this constraint" msgstr "AK(s), die zu diesem Constraint gehören" -#: AKModel/models.py:685 +#: AKModel/models.py:1031 msgid "AK Slot(s) belonging to this constraint" msgstr "AK Slot(s), die zu diesem Constraint gehören" -#: AKModel/models.py:687 +#: AKModel/models.py:1033 msgid "AK Owner belonging to this constraint" msgstr "AK Leitung(en), die zu diesem Constraint gehören" -#: AKModel/models.py:689 +#: AKModel/models.py:1035 msgid "Room belonging to this constraint" msgstr "Raum, der zu diesem Constraint gehört" -#: AKModel/models.py:692 +#: AKModel/models.py:1038 msgid "AK Requirement belonging to this constraint" msgstr "AK Anforderung, die zu diesem Constraint gehört" -#: AKModel/models.py:694 +#: AKModel/models.py:1040 msgid "AK Category belonging to this constraint" msgstr "AK Kategorie, di zu diesem Constraint gehört" -#: AKModel/models.py:696 +#: AKModel/models.py:1042 msgid "Comment" msgstr "Kommentar" -#: AKModel/models.py:696 +#: AKModel/models.py:1042 msgid "Comment or further details for this violation" msgstr "Kommentar oder weitere Details zu dieser Vereletzung" -#: AKModel/models.py:699 +#: AKModel/models.py:1045 msgid "Timestamp" msgstr "Timestamp" -#: AKModel/models.py:699 +#: AKModel/models.py:1045 msgid "Time of creation" msgstr "Zeitpunkt der ERstellung" -#: AKModel/models.py:700 +#: AKModel/models.py:1046 msgid "Manually Resolved" msgstr "Manuell behoben" -#: AKModel/models.py:701 +#: AKModel/models.py:1047 msgid "Mark this violation manually as resolved" msgstr "Markiere diese Verletzung manuell als behoben" -#: AKModel/models.py:728 AKModel/templates/admin/AKModel/aks_by_user.html:22 +#: AKModel/models.py:1074 #: AKModel/templates/admin/AKModel/requirements_overview.html:27 msgid "Details" msgstr "Details" -#: AKModel/models.py:862 +#: AKModel/models.py:1208 msgid "Default Slot" msgstr "Standardslot" -#: AKModel/models.py:867 +#: AKModel/models.py:1213 msgid "Slot End" msgstr "Ende des Slots" -#: AKModel/models.py:867 +#: AKModel/models.py:1213 msgid "Time and date the slot ends" msgstr "Zeit und Datum zu der der Slot endet" -#: AKModel/models.py:872 +#: AKModel/models.py:1218 msgid "Primary categories" msgstr "Primäre Kategorien" -#: AKModel/models.py:873 +#: AKModel/models.py:1219 msgid "Categories that should be assigned to this slot primarily" msgstr "Kategorieren, die diesem Slot primär zugewiesen werden sollen" @@ -953,19 +962,6 @@ msgstr "Bestätigen" msgid "Cancel" msgstr "Abbrechen" -#: AKModel/templates/admin/AKModel/aks_by_user.html:8 -msgid "AKs by Owner" -msgstr "AKs der Leitung" - -#: AKModel/templates/admin/AKModel/aks_by_user.html:26 -#: AKModel/templates/admin/AKModel/requirements_overview.html:31 -msgid "Edit" -msgstr "Bearbeiten" - -#: AKModel/templates/admin/AKModel/aks_by_user.html:33 -msgid "This user does not have any AKs currently" -msgstr "Diese Leitung hat aktuell keine AKs" - #: AKModel/templates/admin/AKModel/event_wizard/activate.html:9 #: AKModel/templates/admin/AKModel/event_wizard/created_prepare_import.html:9 #: AKModel/templates/admin/AKModel/event_wizard/finish.html:9 @@ -1032,12 +1028,16 @@ msgstr "" msgid "Requirements Overview" msgstr "Übersicht Anforderungen" +#: AKModel/templates/admin/AKModel/requirements_overview.html:31 +msgid "Edit" +msgstr "Bearbeiten" + #: AKModel/templates/admin/AKModel/requirements_overview.html:38 msgid "No AKs with this requirement" msgstr "Kein AK mit dieser Anforderung" #: AKModel/templates/admin/AKModel/requirements_overview.html:45 -#: AKModel/views/status.py:179 +#: AKModel/views/status.py:186 msgid "Add Requirement" msgstr "Anforderung hinzufügen" @@ -1090,7 +1090,7 @@ msgstr "Bisher keine Räume" msgid "Active Events" msgstr "Aktive Events" -#: AKModel/templates/admin/ak_index.html:16 AKModel/views/status.py:109 +#: AKModel/templates/admin/ak_index.html:16 AKModel/views/status.py:108 msgid "Scheduling" msgstr "Scheduling" @@ -1123,43 +1123,47 @@ msgstr "Login" msgid "Register" msgstr "Registrieren" -#: AKModel/views/ak.py:17 +#: AKModel/views/ak.py:21 msgid "Requirements for Event" msgstr "Anforderungen für das Event" -#: AKModel/views/ak.py:34 +#: AKModel/views/ak.py:38 msgid "AK CSV Export" msgstr "AK-CSV-Export" -#: AKModel/views/ak.py:48 +#: AKModel/views/ak.py:51 +msgid "AK JSON Export" +msgstr "AK-JSON-Export" + +#: AKModel/views/ak.py:175 msgid "AK Wiki Export" msgstr "AK-Wiki-Export" -#: AKModel/views/ak.py:59 AKModel/views/manage.py:53 +#: AKModel/views/ak.py:186 AKModel/views/manage.py:55 msgid "Wishes" msgstr "Wünsche" -#: AKModel/views/ak.py:71 +#: AKModel/views/ak.py:198 msgid "Delete AK Orga Messages" msgstr "AK-Organachrichten löschen" -#: AKModel/views/ak.py:89 +#: AKModel/views/ak.py:216 msgid "AK Orga Messages successfully deleted" msgstr "AK-Organachrichten erfolgreich gelöscht" -#: AKModel/views/ak.py:101 +#: AKModel/views/ak.py:228 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:102 +#: AKModel/views/ak.py:229 msgid "Reset of interest in AKs successful." msgstr "Interesse an AKs erfolgreich zurückgesetzt." -#: AKModel/views/ak.py:116 +#: AKModel/views/ak.py:243 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:117 +#: AKModel/views/ak.py:244 msgid "AKs' interest counters set back to 0." msgstr "Interessenszähler der AKs zurückgesetzt" @@ -1173,96 +1177,100 @@ msgstr "'%(obj)s' kopiert" msgid "Could not copy '%(obj)s' (%(error)s)" msgstr "'%(obj)s' konnte nicht kopiert werden (%(error)s)" -#: AKModel/views/manage.py:35 AKModel/views/status.py:146 +#: AKModel/views/manage.py:37 AKModel/views/status.py:153 msgid "Export AK Slides" msgstr "AK-Folien exportieren" -#: AKModel/views/manage.py:48 +#: AKModel/views/manage.py:50 msgid "Symbols" msgstr "Symbole" -#: AKModel/views/manage.py:49 +#: AKModel/views/manage.py:51 msgid "Who?" msgstr "Wer?" -#: AKModel/views/manage.py:50 +#: AKModel/views/manage.py:52 msgid "Duration(s)" msgstr "Dauer(n)" -#: AKModel/views/manage.py:51 +#: AKModel/views/manage.py:53 msgid "Reso intention?" msgstr "Resolutionsabsicht?" -#: AKModel/views/manage.py:52 +#: AKModel/views/manage.py:54 msgid "Category (for Wishes)" msgstr "Kategorie (für Wünsche)" -#: AKModel/views/manage.py:101 +#: AKModel/views/manage.py:103 msgid "The following Constraint Violations will be marked as manually resolved" msgstr "" "Die folgenden Constraintverletzungen werden als manuell behoben markiert." -#: AKModel/views/manage.py:102 +#: AKModel/views/manage.py:104 msgid "Constraint Violations marked as resolved" msgstr "Constraintverletzungen als manuell behoben markiert" -#: AKModel/views/manage.py:114 +#: AKModel/views/manage.py:116 msgid "The following Constraint Violations will be set to level 'violation'" msgstr "" "Die folgenden Constraintverletzungen werden auf das Level \"Violation\" " "gesetzt." -#: AKModel/views/manage.py:115 +#: AKModel/views/manage.py:117 msgid "Constraint Violations set to level 'violation'" msgstr "Constraintverletzungen auf Level \"Violation\" gesetzt" -#: AKModel/views/manage.py:127 +#: AKModel/views/manage.py:129 msgid "The following Constraint Violations will be set to level 'warning'" msgstr "" "Die folgenden Constraintverletzungen werden auf das Level 'warning' gesetzt." -#: AKModel/views/manage.py:128 +#: AKModel/views/manage.py:130 msgid "Constraint Violations set to level 'warning'" msgstr "Constraintverletzungen auf Level \"Warning\" gesetzt" -#: AKModel/views/manage.py:140 +#: AKModel/views/manage.py:142 msgid "Publish the plan(s) of:" msgstr "Den Plan/die Pläne veröffentlichen von:" -#: AKModel/views/manage.py:141 +#: AKModel/views/manage.py:143 msgid "Plan published" msgstr "Plan veröffentlicht" -#: AKModel/views/manage.py:153 +#: AKModel/views/manage.py:155 msgid "Unpublish the plan(s) of:" msgstr "Den Plan/die Pläne verbergen von:" -#: AKModel/views/manage.py:154 +#: AKModel/views/manage.py:156 msgid "Plan unpublished" msgstr "Plan verborgen" -#: AKModel/views/manage.py:166 AKModel/views/status.py:130 +#: AKModel/views/manage.py:168 AKModel/views/status.py:129 msgid "Edit Default Slots" msgstr "Standardslots bearbeiten" -#: AKModel/views/manage.py:204 +#: AKModel/views/manage.py:206 #, python-brace-format msgid "Could not update slot {id} since it does not belong to {event}" msgstr "" "Konnte Slot {id} nicht bearbeiten, da er nicht zum Event {event} gehört" -#: AKModel/views/manage.py:235 +#: AKModel/views/manage.py:237 #, python-brace-format msgid "Updated {u} slot(s). created {c} new slot(s) and deleted {d} slot(s)" msgstr "" "{u} Slot(s) aktualisiert, {c} Slot(s) hinzugefügt und {d} Slot(s) gelöscht" +#: AKModel/views/manage.py:257 +msgid "AK JSON Import" +msgstr "AK-JSON-Import" + #: AKModel/views/room.py:37 #, python-format msgid "Created Room '%(room)s'" msgstr "Raum '%(room)s' angelegt" -#: AKModel/views/room.py:51 AKModel/views/status.py:82 +#: AKModel/views/room.py:51 AKModel/views/status.py:81 msgid "Import Rooms from CSV" msgstr "Räume aus CSV importieren" @@ -1280,50 +1288,64 @@ msgstr "{count} Raum/Räume importiert" msgid "No rooms imported" msgstr "Keine Räume importiert" -#: AKModel/views/status.py:17 +#: AKModel/views/status.py:16 msgid "Overview" msgstr "Überblick" -#: AKModel/views/status.py:33 +#: AKModel/views/status.py:32 msgid "Categories" msgstr "Kategorien" -#: AKModel/views/status.py:37 +#: AKModel/views/status.py:36 msgid "Add category" msgstr "Kategorie hinzufügen" -#: AKModel/views/status.py:64 +#: AKModel/views/status.py:63 msgid "Add Room" msgstr "Raum hinzufügen" -#: AKModel/views/status.py:116 +#: AKModel/views/status.py:115 msgid "AKs requiring special attention" msgstr "AKs, die besondere Aufmerksamkeit benötigen" -#: AKModel/views/status.py:122 +#: AKModel/views/status.py:121 msgid "Enter Interest" msgstr "Interesse erfassen" -#: AKModel/views/status.py:134 +#: AKModel/views/status.py:133 msgid "Manage ak tracks" msgstr "AK-Tracks verwalten" -#: AKModel/views/status.py:138 +#: AKModel/views/status.py:137 +msgid "Import AK schedule from JSON" +msgstr "AK-Plan aus JSON importieren" + +#: AKModel/views/status.py:141 msgid "Export AKs as CSV" msgstr "AKs als CSV exportieren" -#: AKModel/views/status.py:142 +#: AKModel/views/status.py:145 +msgid "Export AKs as JSON" +msgstr "AKs als JSON exportieren" + +#: AKModel/views/status.py:149 msgid "Export AKs for Wiki" msgstr "AKs im Wiki-Format exportieren" -#: AKModel/views/status.py:175 +#: AKModel/views/status.py:182 msgid "Show AKs for requirements" msgstr "Zu Anforderungen gehörige AKs anzeigen" -#: AKModel/views/status.py:189 +#: AKModel/views/status.py:196 msgid "Event Status" msgstr "Eventstatus" +#~ msgid "AKs by Owner" +#~ msgstr "AKs der Leitung" + +#~ msgid "This user does not have any AKs currently" +#~ msgstr "Diese Leitung hat aktuell keine AKs" + #~ msgid "Opening time for expression of interest." #~ msgstr "Öffnungszeitpunkt für die Angabe von Interesse an AKs." diff --git a/AKModel/models.py b/AKModel/models.py index 22586a57380b4ba368c440726ecccc3f6d73b1d0..2d58a45651b419899932171a0fdae2a394e85d6b 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -1,18 +1,49 @@ import itertools -from datetime import timedelta +import json +from dataclasses import dataclass +from datetime import datetime, timedelta +from typing import Iterable from django.db import models from django.apps import apps from django.db.models import Count from django.urls import reverse_lazy from django.utils import timezone -from django.utils.datetime_safe import datetime from django.utils.text import slugify from django.utils.translation import gettext_lazy as _ from simple_history.models import HistoricalRecords from timezone_field import TimeZoneField +@dataclass +class OptimizerTimeslot: + """Class describing a timeslot. Used to interface with an optimizer.""" + + avail: "Availability" + idx: int + constraints: set[str] + + def merge(self, other: "OptimizerTimeslot") -> "OptimizerTimeslot": + """Merge with other OptimizerTimeslot. + + Creates a new OptimizerTimeslot object. + Its availability is constructed by merging the availabilities of self and other, + its constraints by taking the union of both constraint sets. + As an index, the index of self is used. + """ + avail = self.avail.merge_with(other.avail) + constraints = self.constraints.union(other.constraints) + # we simply use the index of result[-1] + return OptimizerTimeslot( + avail=avail, idx=self.idx, constraints=constraints + ) + + def __repr__(self) -> str: + return f"({self.avail.simplified}, {self.idx}, {self.constraints})" + +TimeslotBlock = list[OptimizerTimeslot] + + class Event(models.Model): """ An event supplies the frame for all Aks. @@ -162,6 +193,213 @@ class Event(models.Model): .filter(availabilities__count=0, owners__count__gt=0) ) + def _generate_slots_from_block( + self, + start: datetime, + end: datetime, + slot_duration: timedelta, + slot_index: int = 0, + constraints: set[str] | None = None, + ) -> Iterable[TimeslotBlock]: + """Discretize a time range into timeslots. + + Uses a uniform discretization into blocks of length `slot_duration`, + starting at `start`. No incomplete timeslots are generated, i.e. + if (`end` - `start`) is not a whole number multiple of `slot_duration` + then the last incomplete timeslot is dropped. + + :param start: Start of the time range. + :param end: Start of the time range. + :param slot_duration: Duration of a single timeslot in the discretization. + :param slot_index: index of the first timeslot. Defaults to 0. + + :yield: Block of optimizer timeslots as the discretization result. + :ytype: list of TimeslotBlock + """ + # local import to prevent cyclic import + # pylint: disable=import-outside-toplevel + from AKModel.availability.models import Availability + + current_slot_start = start + previous_slot_start: datetime | None = None + + if constraints is None: + constraints = set() + + current_block = [] + + room_availabilities = list({ + availability + for room in Room.objects.filter(event=self) + for availability in room.availabilities.all() + }) + + while current_slot_start + slot_duration <= end: + slot = Availability( + 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 + ): + yield current_block + current_block = [] + + current_block.append( + OptimizerTimeslot(avail=slot, idx=slot_index, constraints=constraints) + ) + previous_slot_start = current_slot_start + + slot_index += 1 + current_slot_start += slot_duration + + if current_block: + yield current_block + + return slot_index + + def uniform_time_slots(self, *, slots_in_an_hour: float = 1.0) -> Iterable[TimeslotBlock]: + """Uniformly discretize the entire event into a single block of timeslots. + + :param slots_in_an_hour: The percentage of an hour covered by a single slot. + Determines the discretization granularity. + :yield: Block of optimizer timeslots as the discretization result. + :ytype: a single list of TimeslotBlock + """ + all_category_constraints = AKCategory.create_category_constraints( + 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, + ) + + def default_time_slots(self, *, slots_in_an_hour: float = 1.0) -> Iterable[TimeslotBlock]: + """Discretize all default slots into blocks of timeslots. + + In the discretization each default slot corresponds to one block. + + :param slots_in_an_hour: The percentage of an hour covered by a single slot. + Determines the discretization granularity. + :yield: Block of optimizer timeslots as the discretization result. + :ytype: list of TimeslotBlock + """ + slot_duration = timedelta(hours=1.0 / slots_in_an_hour) + slot_index = 0 + + for block_slot in DefaultSlot.objects.filter(event=self).order_by("start", "end"): + category_constraints = AKCategory.create_category_constraints( + 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, + ) + + def merge_blocks( + self, blocks: Iterable[TimeslotBlock] + ) -> Iterable[TimeslotBlock]: + """Merge iterable of blocks together. + + The timeslots of all blocks are grouped into maximal blocks. + Timeslots with the same start and end are identified with each other + and merged (cf `OptimizerTimeslot.merge`). + Throws a ValueError if any timeslots are overlapping but do not + share the same start and end, i.e. partial overlap is not allowed. + + :param blocks: iterable of blocks to merge. + :return: iterable of merged blocks. + :rtype: iterable over lists of OptimizerTimeslot objects + """ + if not blocks: + return [] + + # flatten timeslot iterables to single chain + timeslot_chain = itertools.chain.from_iterable(blocks) + + # sort timeslots according to start + timeslots = sorted( + timeslot_chain, + key=lambda slot: slot.avail.start + ) + + if not timeslots: + return [] + + all_blocks = [] + current_block = [timeslots[0]] + timeslots = timeslots[1:] + + 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 + ): + # the same timeslot -> merge + current_block[-1] = current_block[-1].merge(slot) + else: + # 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})" + ) + elif not current_block or slot.avail.overlaps(current_block[-1].avail, strict=False): + # only endpoints in intersection -> same block + current_block.append(slot) + else: + # no overlap at all -> new block + all_blocks.append(current_block) + current_block = [slot] + + if current_block: + all_blocks.append(current_block) + + return all_blocks + + def schedule_from_json(self, schedule: str) -> None: + """Load AK schedule from a json string. + + :param schedule: A string that can be decoded to json, describing + the AK schedule. The json data is assumed to be constructed + following the output specification of the KoMa conference optimizer, cf. + https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format + """ + schedule = json.loads(schedule) + + slots_in_an_hour = schedule["input"]["timeslots"]["info"]["duration"] + + timeslot_dict = { + timeslot.idx: timeslot + for block in self.merge_blocks(self.default_time_slots(slots_in_an_hour=slots_in_an_hour)) + for timeslot in block + } + + for scheduled_slot in schedule["scheduled_aks"]: + slot = AKSlot.objects.get(id=int(scheduled_slot["ak_id"])) + slot.room = Room.objects.get(id=int(scheduled_slot["room_id"])) + + scheduled_slot["timeslot_ids"] = list(map(int, scheduled_slot["timeslot_ids"])) + + start_timeslot = timeslot_dict[min(scheduled_slot["timeslot_ids"])].avail + end_timeslot = timeslot_dict[max(scheduled_slot["timeslot_ids"])].avail + + slot.start = start_timeslot.start + slot.duration = (end_timeslot.end - start_timeslot.start).total_seconds() / 3600.0 + slot.save() class AKOwner(models.Model): """ An AKOwner describes the person organizing/holding an AK. @@ -260,6 +498,20 @@ class AKCategory(models.Model): def __str__(self): return self.name + @staticmethod + def create_category_constraints(categories: Iterable["AKCategory"]) -> set[str]: + """Create a set of constraint strings from an AKCategory iterable. + + :param categories: The iterable of categories to derive the constraint strings from. + :return: A set of category constraint strings, i.e. strings of the form + 'availability-cat-<cat.name>'. + :rtype: set of strings. + """ + return { + f"availability-cat-{cat.name}" + for cat in categories + } + class AKTrack(models.Model): """ An AKTrack describes a set of semantically related AKs. @@ -513,6 +765,43 @@ class Room(models.Model): def __str__(self): return self.title + def as_json(self) -> str: + """Return a json string representation of this room object. + + :return: The json string representation is constructed + following the input specification of the KoMa conference optimizer, cf. + https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format + :rtype: str + """ + # local import to prevent cyclic import + # pylint: disable=import-outside-toplevel + from AKModel.availability.models import Availability + + # check if room is available for the whole event + # -> no time constraint needs to be introduced + if Availability.is_event_covered(self.event, self.availabilities.all()): + time_constraints = [] + else: + time_constraints = [f"availability-room-{self.pk}"] + + data = { + "id": str(self.pk), + "info": { + "name": self.name, + }, + "capacity": self.capacity, + "fulfilled_room_constraints": [constraint.name + for constraint in self.properties.all()], + "time_constraints": time_constraints + } + + data["fulfilled_room_constraints"].append(f"availability-room-{self.pk}") + + if not any(constr.startswith("proxy") for constr in data["fulfilled_room_constraints"]): + data["fulfilled_room_constraints"].append("no-proxy") + + return json.dumps(data) + class AKSlot(models.Model): """ An AK Mapping matches an AK to a room during a certain time. @@ -608,6 +897,63 @@ class AKSlot(models.Model): self.duration = min(self.duration, event_duration_hours) super().save(force_insert, force_update, using, update_fields) + def as_json(self) -> str: + """Return a json string representation of the AK object of this slot. + + :return: The json string representation is constructed + following the input specification of the KoMa conference optimizer, cf. + https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format + :rtype: str + """ + # local import to prevent cyclic import + # pylint: disable=import-outside-toplevel + from AKModel.availability.models import Availability + + # check if ak resp. owner is available for the whole event + # -> no time constraint needs to be introduced + + if not self.fixed and Availability.is_event_covered(self.event, self.ak.availabilities.all()): + ak_time_constraints = [] + else: + ak_time_constraints = [f"availability-ak-{self.ak.pk}"] + + def _owner_time_constraints(owner: AKOwner): + if Availability.is_event_covered(self.event, owner.availabilities.all()): + return [] + return [f"availability-person-{owner.pk}"] + + # self.slots_in_an_hour is set in AKJSONExportView + data = { + "id": str(self.pk), + "duration": int(self.duration * self.slots_in_an_hour), + "properties": {}, + "room_constraints": [constraint.name + for constraint in self.ak.requirements.all()], + "time_constraints": ["resolution"] if self.ak.reso else [], + "info": { + "name": self.ak.name, + "head": ", ".join([str(owner) + for owner in self.ak.owners.all()]), + "description": self.ak.description, + "reso": self.ak.reso, + }, + } + + data["time_constraints"].extend(ak_time_constraints) + for owner in self.ak.owners.all(): + data["time_constraints"].extend(_owner_time_constraints(owner)) + + if self.ak.category: + category_constraints = AKCategory.create_category_constraints([self.ak.category]) + data["time_constraints"].extend(category_constraints) + + if self.room is not None and self.fixed: + data["room_constraints"].append(f"availability-room-{self.room.pk}") + + if not any(constr.startswith("proxy") for constr in data["room_constraints"]): + data["room_constraints"].append("no-proxy") + + return json.dumps(data) class AKOrgaMessage(models.Model): """ diff --git a/AKModel/templates/admin/AKModel/ak_json_export.html b/AKModel/templates/admin/AKModel/ak_json_export.html new file mode 100644 index 0000000000000000000000000000000000000000..38e5526edc8364faf75491e68cb893b10d64751a --- /dev/null +++ b/AKModel/templates/admin/AKModel/ak_json_export.html @@ -0,0 +1,20 @@ +{% extends "admin/base_site.html" %} + +{% load tz %} + +{% block content %} +<pre> + {"aks": [ + {% for slot in slots %}{{ slot.as_json }}{% if not forloop.last %}, + {% endif %}{% endfor %} + ], + "rooms": [ + {% for room in rooms %}{{ room.as_json }}{% if not forloop.last %}, + {% endif %}{% endfor %} + ], + "participants": {{ participants }}, + "timeslots": {{ timeslots }}, + "info": {{ info_dict }} + } +</pre> +{% endblock %} diff --git a/AKModel/urls.py b/AKModel/urls.py index 3abf646058afdf71c7938032236942e7bb0ed995..9871b4119949d31350ecc64db568d515b61eb3cb 100644 --- a/AKModel/urls.py +++ b/AKModel/urls.py @@ -5,8 +5,9 @@ from rest_framework.routers import DefaultRouter import AKModel.views.api from AKModel.views.manage import ExportSlidesView, PlanPublishView, PlanUnpublishView, DefaultSlotEditorView, \ - AKsByUserView -from AKModel.views.ak import AKRequirementOverview, AKCSVExportView, AKWikiExportView, AKMessageDeleteView + AKsByUserView, AKJSONImportView +from AKModel.views.ak import AKRequirementOverview, AKCSVExportView, AKJSONExportView, AKWikiExportView, \ + AKMessageDeleteView from AKModel.views.event_wizard import NewEventWizardStartView, NewEventWizardPrepareImportView, \ NewEventWizardImportView, NewEventWizardActivateView, NewEventWizardFinishView, NewEventWizardSettingsView from AKModel.views.room import RoomBatchCreationView @@ -96,6 +97,10 @@ def get_admin_urls_event(admin_site): name="aks_by_owner"), path('<slug:event_slug>/ak-csv-export/', admin_site.admin_view(AKCSVExportView.as_view()), name="ak_csv_export"), + path('<slug:event_slug>/ak-json-export/', admin_site.admin_view(AKJSONExportView.as_view()), + name="ak_json_export"), + path('<slug:event_slug>/ak-json-import/', admin_site.admin_view(AKJSONImportView.as_view()), + name="ak_json_import"), path('<slug:slug>/ak-wiki-export/', admin_site.admin_view(AKWikiExportView.as_view()), name="ak_wiki_export"), path('<slug:event_slug>/delete-orga-messages/', admin_site.admin_view(AKMessageDeleteView.as_view()), diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index 3afec5ac2de03a62ad3d103d17062927c3b97026..76bfdb284e42cc71dc9be791a171ba762e2a9b5f 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -1,11 +1,15 @@ +import json +from typing import List + from django.contrib import messages from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ from django.views.generic import ListView, DetailView +from AKModel.availability.models import Availability from AKModel.metaviews.admin import AdminViewMixin, FilterByEventSlugMixin, EventSlugMixin, IntermediateAdminView, \ IntermediateAdminActionView -from AKModel.models import AKRequirement, AKSlot, Event, AKOrgaMessage, AK +from AKModel.models import AKRequirement, AKSlot, Event, AKOrgaMessage, AK, Room, AKOwner class AKRequirementOverview(AdminViewMixin, FilterByEventSlugMixin, ListView): @@ -37,6 +41,129 @@ class AKCSVExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): return super().get_queryset().order_by("ak__track") +class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): + """ + View: Export all AK slots of this event in JSON format ordered by tracks + """ + template_name = "admin/AKModel/ak_json_export.html" + model = AKSlot + context_object_name = "slots" + title = _("AK JSON Export") + + + def _test_slot_contained(self, slot: Availability, availabilities: List[Availability]) -> bool: + return any(availability.contains(slot) for availability in availabilities) + + def _test_event_covered(self, availabilities: List[Availability]) -> bool: + return not Availability.is_event_covered(self.event, availabilities) + + def _test_fixed_ak(self, ak_id, slot: Availability, ak_fixed: dict) -> bool: + if not ak_id in ak_fixed: + return False + + fixed_slot = Availability(self.event, start=ak_fixed[ak_id].start, end=ak_fixed[ak_id].end) + return fixed_slot.overlaps(slot, strict=True) + + def _test_add_constraint(self, slot: Availability, availabilities: List[Availability]) -> bool: + return ( + self._test_event_covered(availabilities) + and self._test_slot_contained(slot, availabilities) + ) + + def get_queryset(self): + return super().get_queryset().order_by("ak__track") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["participants"] = json.dumps([]) + + rooms = Room.objects.filter(event=self.event) + context["rooms"] = rooms + + # TODO: Configure magic number in event + SLOTS_IN_AN_HOUR = 1 + + timeslots = { + "info": {"duration": (1.0 / SLOTS_IN_AN_HOUR), }, + "blocks": [], + } + + for slot in context["slots"]: + slot.slots_in_an_hour = SLOTS_IN_AN_HOUR + + ak_availabilities = { + slot.ak.pk: Availability.union(slot.ak.availabilities.all()) + for slot in context["slots"] + } + room_availabilities = { + room.pk: Availability.union(room.availabilities.all()) + for room in rooms + } + person_availabilities = { + person.pk: Availability.union(person.availabilities.all()) + for person in AKOwner.objects.filter(event=self.event) + } + + ak_fixed = { + ak_id: values.get() + for ak_id in ak_availabilities.keys() + if (values := AKSlot.objects.select_related().filter(ak__pk=ak_id, fixed=True)).exists() + } + + for block in self.event.merge_blocks(self.event.default_time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR)): + current_block = [] + + for timeslot in block: + time_constraints = [] + if self.event.reso_deadline is None or timeslot.avail.end < self.event.reso_deadline: + time_constraints.append("resolution") + + time_constraints.extend([ + f"availability-ak-{ak_id}" + for ak_id, availabilities in ak_availabilities.items() + if ( + self._test_add_constraint(timeslot.avail, availabilities) + or self._test_fixed_ak(ak_id, timeslot.avail, ak_fixed) + ) + ]) + time_constraints.extend([ + f"availability-person-{person_id}" + for person_id, availabilities in person_availabilities.items() + if self._test_add_constraint(timeslot.avail, availabilities) + ]) + time_constraints.extend([ + f"availability-room-{room_id}" + for room_id, availabilities in room_availabilities.items() + if self._test_add_constraint(timeslot.avail, availabilities) + ]) + time_constraints.extend(timeslot.constraints) + + current_block.append({ + "id": str(timeslot.idx), + "info": { + "start": timeslot.avail.simplified, + }, + "fulfilled_time_constraints": time_constraints, + }) + + timeslots["blocks"].append(current_block) + + context["timeslots"] = json.dumps(timeslots) + + info_dict = { + "title": self.event.name, + "slug": self.event.slug + } + for attr in ["contact_email", "place"]: + if hasattr(self.event, attr) and getattr(self.event, attr): + info_dict[attr] = getattr(self.event, attr) + + context["info_dict"] = json.dumps(info_dict) + + return context + + + class AKWikiExportView(AdminViewMixin, DetailView): """ View: Export AKs of this event in wiki syntax diff --git a/AKModel/views/manage.py b/AKModel/views/manage.py index 64443cb8f7de27832f518df75378f1fc2ea59571..ec5076fbd850c71a6f7d1a1d202a46461e43ebbd 100644 --- a/AKModel/views/manage.py +++ b/AKModel/views/manage.py @@ -4,15 +4,17 @@ import os import tempfile from itertools import zip_longest + from django.contrib import messages from django.db.models.functions import Now +from django.shortcuts import redirect from django.utils.dateparse import parse_datetime from django.utils.translation import gettext_lazy as _ from django.views.generic import TemplateView, DetailView from django_tex.core import render_template_with_context, run_tex_in_directory from django_tex.response import PDFResponse -from AKModel.forms import SlideExportForm, DefaultSlotEditorForm +from AKModel.forms import SlideExportForm, DefaultSlotEditorForm, JSONImportForm from AKModel.metaviews.admin import EventSlugMixin, IntermediateAdminView, IntermediateAdminActionView, AdminViewMixin from AKModel.models import ConstraintViolation, Event, DefaultSlot, AKOwner @@ -58,7 +60,7 @@ class ExportSlidesView(EventSlugMixin, IntermediateAdminView): Create a list of tuples cosisting of an AK and a list of upcoming AKs (list length depending on setting) """ next_aks_list = zip_longest(*[ak_list[i + 1:] for i in range(NEXT_AK_LIST_LENGTH)], fillvalue=None) - return [(ak, next_aks) for ak, next_aks in zip_longest(ak_list, next_aks_list, fillvalue=[])] + return list(zip_longest(ak_list, next_aks_list, fillvalue=[])) # Get all relevant AKs (wishes separately, and either all AKs or only those who should directly or indirectly # be presented when restriction setting was chosen) @@ -245,3 +247,16 @@ class AKsByUserView(AdminViewMixin, EventSlugMixin, DetailView): model = AKOwner context_object_name = 'owner' template_name = "admin/AKModel/aks_by_user.html" + + +class AKJSONImportView(EventSlugMixin, IntermediateAdminView): + """ + View: Import an AK schedule from a json file that can be pasted into this view. + """ + form_class = JSONImportForm + title = _("AK JSON Import") + + def form_valid(self, form): + self.event.schedule_from_json(form.data["json_data"]) + + return redirect("admin:event_status", self.event.slug) diff --git a/AKModel/views/status.py b/AKModel/views/status.py index e14ce2fbbcc7c662f71d1711a6fa14b372fb3595..0c12b30348c63d6178f0b5c38d2fcec6cfebf664 100644 --- a/AKModel/views/status.py +++ b/AKModel/views/status.py @@ -133,10 +133,18 @@ class EventAKsWidget(TemplateStatusWidget): "text": _("Manage ak tracks"), "url": reverse_lazy("admin:tracks_manage", kwargs={"event_slug": context["event"].slug}), }, + { + "text": _("Import AK schedule from JSON"), + "url": reverse_lazy("admin:ak_json_import", kwargs={"event_slug": context["event"].slug}), + }, { "text": _("Export AKs as CSV"), "url": reverse_lazy("admin:ak_csv_export", kwargs={"event_slug": context["event"].slug}), }, + { + "text": _("Export AKs as JSON"), + "url": reverse_lazy("admin:ak_json_export", kwargs={"event_slug": context["event"].slug}), + }, { "text": _("Export AKs for Wiki"), "url": reverse_lazy("admin:ak_wiki_export", kwargs={"slug": context["event"].slug}), diff --git a/AKPlan/locale/de_DE/LC_MESSAGES/django.po b/AKPlan/locale/de_DE/LC_MESSAGES/django.po index 90ee78c19d2da3dc385e336b58a4b37d6b76c000..1bbfe72524190a8feb747dfe0e0c2e6ae88d09a6 100644 --- a/AKPlan/locale/de_DE/LC_MESSAGES/django.po +++ b/AKPlan/locale/de_DE/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-05-15 20:03+0200\n" +"POT-Creation-Date: 2024-05-27 01:57+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" @@ -38,7 +38,7 @@ msgstr "Veranstaltung" #: AKPlan/templates/AKPlan/plan_index.html:59 #: AKPlan/templates/AKPlan/plan_room.html:13 #: AKPlan/templates/AKPlan/plan_room.html:59 -#: AKPlan/templates/AKPlan/plan_wall.html:65 +#: AKPlan/templates/AKPlan/plan_wall.html:67 msgid "Room" msgstr "Raum" @@ -63,12 +63,12 @@ msgid "AK Wall" msgstr "AK-Wall" #: AKPlan/templates/AKPlan/plan_index.html:130 -#: AKPlan/templates/AKPlan/plan_wall.html:130 +#: AKPlan/templates/AKPlan/plan_wall.html:132 msgid "Current AKs" msgstr "Aktuelle AKs" #: AKPlan/templates/AKPlan/plan_index.html:137 -#: AKPlan/templates/AKPlan/plan_wall.html:135 +#: AKPlan/templates/AKPlan/plan_wall.html:137 msgid "Next AKs" msgstr "Nächste AKs" @@ -99,7 +99,7 @@ msgstr "Eigenschaften" msgid "Track" msgstr "Track" -#: AKPlan/templates/AKPlan/plan_wall.html:145 +#: AKPlan/templates/AKPlan/plan_wall.html:147 msgid "Reload page automatically?" msgstr "Seite automatisch neu laden?" diff --git a/AKPlanning/locale/de_DE/LC_MESSAGES/django.po b/AKPlanning/locale/de_DE/LC_MESSAGES/django.po index 3f47223fa6145f6d4c55f87c6f4c1c660f2bd311..68529ec4e06f679135124da17e2aa86e6d2757a7 100644 --- a/AKPlanning/locale/de_DE/LC_MESSAGES/django.po +++ b/AKPlanning/locale/de_DE/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-08-16 16:30+0200\n" +"POT-Creation-Date: 2024-05-27 01:57+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" @@ -17,10 +17,10 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: AKPlanning/settings.py:148 +#: AKPlanning/settings.py:147 msgid "German" msgstr "Deutsch" -#: AKPlanning/settings.py:149 +#: AKPlanning/settings.py:148 msgid "English" msgstr "Englisch" diff --git a/AKScheduling/models.py b/AKScheduling/models.py index 1495f311935583a945aef0d737ca44e21a0a2663..aa6be3d0398ea82461db295ce9e853dfd27386d9 100644 --- a/AKScheduling/models.py +++ b/AKScheduling/models.py @@ -288,6 +288,8 @@ def ak_requirements_changed_handler(sender, instance: AK, action: str, **kwargs) for slot in slots_of_this_ak: room = slot.room + if room is None: + continue room_requirements = room.properties.all() for requirement in instance.requirements.all(): diff --git a/AKSubmission/locale/de_DE/LC_MESSAGES/django.po b/AKSubmission/locale/de_DE/LC_MESSAGES/django.po index b25db7600b98a97b8498ea72067f9504f5b2b1c8..e8add44b31100539ff4a2b8311b40ffba14c4b8a 100644 --- a/AKSubmission/locale/de_DE/LC_MESSAGES/django.po +++ b/AKSubmission/locale/de_DE/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-08-16 16:30+0200\n" +"POT-Creation-Date: 2024-05-27 01:57+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" @@ -17,16 +17,16 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: AKSubmission/forms.py:93 +#: AKSubmission/forms.py:95 #, python-format msgid "\"%(duration)s\" is not a valid duration" msgstr "\"%(duration)s\" ist keine gültige Dauer" -#: AKSubmission/forms.py:159 +#: AKSubmission/forms.py:155 msgid "Duration(s)" msgstr "Dauer(n)" -#: AKSubmission/forms.py:161 +#: AKSubmission/forms.py:157 msgid "" "Enter at least one planned duration (in hours). If your AK should have " "multiple slots, use multiple lines" diff --git a/INSTALL.md b/INSTALL.md index c887af92dba1d2661b5b53ef9a17041d2c1a730d..5344a998cffe2603aaaffb0583e91fb43e533c38 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -10,7 +10,7 @@ setup. ### System Requirements -* Python 3.8+ incl. development tools +* Python 3.10+ 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.7`` +1. setup a virtual environment using the proper python version ``virtualenv venv -p python3.10`` 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.7`` +1. setup a virtual environment using the proper python version ``virtualenv venv -p python3.10`` 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/setup.sh b/Utils/setup.sh index 1c951824905e99e4650f40a562b633b92d7b8b02..6a93207d197e75da2875b31ea8e0e631e114e837 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.9 +virtualenv venv -p python3.10 source venv/bin/activate pip install --upgrade setuptools pip wheel pip install -r requirements.txt