From d237c85f81beae4491cc75c15fc93fcb48d7e20b Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Mon, 27 May 2024 04:38:46 +0200 Subject: [PATCH 01/77] JSON import and export code, migrated from mmarx fork --- AKModel/forms.py | 9 ++ AKModel/models.py | 77 +++++++++++++++++ .../admin/AKModel/ak_json_export.html | 19 +++++ AKModel/urls.py | 9 +- AKModel/views/ak.py | 85 +++++++++++++++++++ AKModel/views/manage.py | 12 ++- AKModel/views/status.py | 8 ++ 7 files changed, 216 insertions(+), 3 deletions(-) create mode 100644 AKModel/templates/admin/AKModel/ak_json_export.html diff --git a/AKModel/forms.py b/AKModel/forms.py index 4d1fe7ef..f74dc39e 100644 --- a/AKModel/forms.py +++ b/AKModel/forms.py @@ -272,3 +272,12 @@ 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): + json_data = forms.CharField( + required=True, + widget=forms.Textarea, + label=_("JSON data"), + help_text=_("JSON data from the scheduling solver"), + ) diff --git a/AKModel/models.py b/AKModel/models.py index 22586a57..c4c6f804 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -1,4 +1,5 @@ import itertools +import json from datetime import timedelta from django.db import models @@ -162,6 +163,66 @@ class Event(models.Model): .filter(availabilities__count=0, owners__count__gt=0) ) + def time_slots(self, *, slots_in_an_hour=1.0): + from AKModel.availability.models import Availability + + rooms = Room.objects.filter(event=self) + slot_duration = timedelta(hours=(1.0 / slots_in_an_hour)) + slot_index = 0 + current_slot = self.start + current_block = [] + previous_slot = None + + room_availabilities = list({availability + for room in rooms + for availability in room.availabilities.all()}) + + while current_slot < self.end: + slot = Availability(event=self, + start=current_slot, + end=current_slot + slot_duration) + + if any((availability.contains(slot) + for availability in room_availabilities)): + if previous_slot is not None and previous_slot + slot_duration < current_slot: + yield current_block + current_block = [] + + current_block.append(slot_index) + previous_slot = current_slot + + slot_index += 1 + current_slot += slot_duration + + yield current_block + + def time_slot(self, *, time_slot_index, slots_in_an_hour=1.0): + from AKModel.availability.models import Availability + slot_duration = timedelta(hours=(1.0 / slots_in_an_hour)) + + start = self.start + time_slot_index * slot_duration + + return Availability(event=self, + start=start, + end=start + slot_duration) + + def schedule_from_json(self, schedule): + schedule = json.loads(schedule) + + slots_in_an_hour = schedule["input"]["timeslots"]["info"]["duration"] + + for scheduled_slot in schedule["scheduled_aks"]: + slot = AKSlot.objects.get(scheduled_slot["ak_id"]) + slot.room = scheduled_slot["room_id"] + + start = min(scheduled_slot["time_slot_ids"]) + end = max(scheduled_slot["time_slot_ids"]) + + slot.start = self.time_slot(time_slot_index=start, + slots_in_an_hour=slots_in_an_hour) + slot.end = self.time_slot(time_slot_index=end + 1, + slots_in_an_hour=slots_in_an_hour) + class AKOwner(models.Model): """ An AKOwner describes the person organizing/holding an AK. @@ -236,6 +297,22 @@ class AKOwner(models.Model): """ return AKOwner.objects.get(event=event, slug=slug) + def as_json(self) -> str: + data = { + "id": self.pk, + "info": { + "name": self.name, + }, + "capacity": self.capacity, + "fulfilled_room_constraints": [constraint.name + for constraint in self.properties.all()], + "time_constraints": [f"availability-room-{self.pk}"] + } + + return json.dumps(data) + + + class AKCategory(models.Model): """ An AKCategory describes the characteristics of an AK, e.g. content vs. recreational. 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 00000000..da582504 --- /dev/null +++ b/AKModel/templates/admin/AKModel/ak_json_export.html @@ -0,0 +1,19 @@ +{% 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 }} + } +</pre> +{% endblock %} diff --git a/AKModel/urls.py b/AKModel/urls.py index 3abf6460..9871b411 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 3afec5ac..547f3606 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -1,3 +1,6 @@ +import json +from datetime import timedelta + from django.contrib import messages from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ @@ -37,6 +40,88 @@ 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 get_queryset(self): + return super().get_queryset().order_by("ak__track") + + def get_context_data(self, **kwargs): + from AKModel.availability.models import Availability + + SLOTS_IN_AN_HOUR = 1 + + rooms = Room.objects.filter(event=self.event) + participants = [] + timeslots = { + "info": {"duration": (1.0 / SLOTS_IN_AN_HOUR), }, + "blocks": [], + } + + context = super().get_context_data(**kwargs) + context["rooms"] = rooms + context["participants"] = json.dumps(participants) + + for slot in context["slots"]: + slot.slots_in_an_hour = SLOTS_IN_AN_HOUR + + ak_availabilities = {slot.ak.pk: availability + for slot in context["slots"] + for availability in slot.ak.availabilities.all()} + room_availabilities = {room.pk: availability + for room in rooms + for availability in room.availabilities.all()} + person_availabilities = {person.pk: availability + for person in AKOwner.objects.filter(event=self.event) + for availability in person.availabilities.all()} + + + + for block in self.event.time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR): + current_block = [] + + for slot_index in block: + slot = self.event.time_slot(time_slot_index=slot_index, + slots_in_an_hour=SLOTS_IN_AN_HOUR) + constraints = [] + + if slot.end < self.event.reso_deadline: + constraints.append("resolution") + + for (ak, availability) in ak_availabilities.items(): + if availability.contains(slot): + constraints.append(f"availability-ak-{ak}") + + for (person, availability) in person_availabilities.items(): + if availability.contains(slot): + constraints.append(f"availability-person-{person}") + + for (room, availability) in room_availabilities.items(): + if availability.contains(slot): + constraints.append(f"availability-room-{room}") + + current_block.append({ + "id": slot_index, + "info": { + "start": slot.simplified, + }, + "fulfilled_time_constraints": constraints, + }) + + timeslots["blocks"].append(current_block) + + context["timeslots"] = json.dumps(timeslots) + + 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 64443cb8..0ed33044 100644 --- a/AKModel/views/manage.py +++ b/AKModel/views/manage.py @@ -12,7 +12,7 @@ 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 @@ -245,3 +245,13 @@ class AKsByUserView(AdminViewMixin, EventSlugMixin, DetailView): model = AKOwner context_object_name = 'owner' template_name = "admin/AKModel/aks_by_user.html" + + +class AKJSONImportView(EventSlugMixin, IntermediateAdminView): + 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 e14ce2fb..0c12b303 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}), -- GitLab From 1db999b14dd2ce7b2fd32b4481ca268a4515a969 Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Mon, 27 May 2024 04:40:18 +0200 Subject: [PATCH 02/77] Update language files --- AKModel/locale/de_DE/LC_MESSAGES/django.po | 452 +++++++++--------- AKPlan/locale/de_DE/LC_MESSAGES/django.po | 10 +- AKPlanning/locale/de_DE/LC_MESSAGES/django.po | 6 +- .../locale/de_DE/LC_MESSAGES/django.po | 8 +- 4 files changed, 249 insertions(+), 227 deletions(-) diff --git a/AKModel/locale/de_DE/LC_MESSAGES/django.po b/AKModel/locale/de_DE/LC_MESSAGES/django.po index 261c5b2c..1026edf9 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-27 02:31+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,10 +33,9 @@ msgstr "Plan veröffentlichen" 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:437 AKModel/models.py:759 #: AKModel/templates/admin/AKModel/status/event_aks.html:10 -#: AKModel/views/manage.py:73 AKModel/views/status.py:98 +#: AKModel/views/manage.py:73 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:184 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:199 msgid "Reset AKs' interest counters" msgstr "Interessenszähler der AKs zurücksetzen" @@ -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:61 AKModel/models.py:235 +#: AKModel/models.py:328 AKModel/models.py:347 AKModel/models.py:373 +#: AKModel/models.py:427 AKModel/models.py:569 AKModel/models.py:608 +#: AKModel/models.py:698 AKModel/models.py:755 AKModel/models.py:946 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:236 +#: AKModel/models.py:329 AKModel/models.py:348 AKModel/models.py:374 +#: AKModel/models.py:428 AKModel/models.py:570 AKModel/models.py:609 +#: AKModel/models.py:699 AKModel/models.py:756 AKModel/models.py:947 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:573 +#: AKModel/models.py:598 AKModel/models.py:765 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:436 +#: AKModel/models.py:597 AKModel/models.py:693 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:332 +#: AKModel/models.py:771 msgid "AK Category" msgstr "AK-Kategorie" @@ -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:940 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:281 +msgid "JSON data" +msgstr "JSON-Daten" + +#: AKModel/forms.py:282 +msgid "JSON data from the scheduling solver" +msgstr "JSON-Daten, die der scheduling-solver produziert hat" + +#: AKModel/metaviews/admin.py:156 AKModel/models.py:30 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:21 AKModel/models.py:320 AKModel/models.py:344 +#: AKModel/models.py:371 AKModel/models.py:389 AKModel/models.py:561 msgid "Name" msgstr "Name" -#: AKModel/models.py:21 +#: AKModel/models.py:22 msgid "Name or iteration of the event" msgstr "Name oder Iteration des Events" -#: AKModel/models.py:22 +#: AKModel/models.py:23 msgid "Short Form" msgstr "Kurzer Name" -#: AKModel/models.py:23 +#: AKModel/models.py:24 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:26 msgid "Place" msgstr "Ort" -#: AKModel/models.py:26 +#: AKModel/models.py:27 msgid "City etc. the event takes place in" msgstr "Stadt o.ä. in der das Event stattfindet" -#: AKModel/models.py:28 +#: AKModel/models.py:29 msgid "Time Zone" msgstr "Zeitzone" -#: AKModel/models.py:28 +#: AKModel/models.py:29 msgid "Time Zone where this event takes place in" msgstr "Zeitzone in der das Event stattfindet" -#: AKModel/models.py:29 +#: AKModel/models.py:30 msgid "Time the event begins" msgstr "Zeit zu der das Event beginnt" -#: AKModel/models.py:30 +#: AKModel/models.py:31 msgid "End" msgstr "Ende" -#: AKModel/models.py:30 +#: AKModel/models.py:31 msgid "Time the event ends" msgstr "Zeit zu der das Event endet" -#: AKModel/models.py:31 +#: AKModel/models.py:32 msgid "Resolution Deadline" msgstr "Resolutionsdeadline" -#: AKModel/models.py:32 +#: AKModel/models.py:33 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:35 msgid "Interest Window Start" msgstr "Beginn Interessensbekundung" -#: AKModel/models.py:36 +#: AKModel/models.py:37 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:39 msgid "Interest Window End" msgstr "Ende Interessensbekundung" -#: AKModel/models.py:39 +#: AKModel/models.py:40 msgid "Closing time for expression of interest." msgstr "Öffnungszeitpunkt für die Angabe von Interesse an AKs." -#: AKModel/models.py:41 +#: AKModel/models.py:42 msgid "Public event" msgstr "Öffentliches Event" -#: AKModel/models.py:42 +#: AKModel/models.py:43 msgid "Show this event on overview page." msgstr "Zeige dieses Event auf der Übersichtseite an" -#: AKModel/models.py:44 +#: AKModel/models.py:45 msgid "Active State" msgstr "Aktiver Status" -#: AKModel/models.py:44 +#: AKModel/models.py:45 msgid "Marks currently active events" msgstr "Markiert aktuell aktive Events" -#: AKModel/models.py:45 +#: AKModel/models.py:46 msgid "Plan Hidden" msgstr "Plan verborgen" -#: AKModel/models.py:45 +#: AKModel/models.py:46 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:48 msgid "Plan published at" msgstr "Plan veröffentlicht am/um" -#: AKModel/models.py:48 +#: AKModel/models.py:49 msgid "Timestamp at which the plan was published" msgstr "Zeitpunkt, zu dem der Plan veröffentlicht wurde" -#: AKModel/models.py:50 +#: AKModel/models.py:51 msgid "Base URL" msgstr "URL-Prefix" -#: AKModel/models.py:50 +#: AKModel/models.py:51 msgid "Prefix for wiki link construction" msgstr "Prefix für die automatische Generierung von Wiki-Links" -#: AKModel/models.py:51 +#: AKModel/models.py:52 msgid "Wiki Export Template Name" msgstr "Wiki-Export Templatename" -#: AKModel/models.py:52 +#: AKModel/models.py:53 msgid "Default Slot Length" msgstr "Standardslotlänge" -#: AKModel/models.py:53 +#: AKModel/models.py:54 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:56 msgid "Contact email address" msgstr "E-Mail Kontaktadresse" -#: AKModel/models.py:56 +#: AKModel/models.py:57 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:62 msgid "Events" msgstr "Events" -#: AKModel/models.py:169 +#: AKModel/models.py:230 msgid "Nickname" msgstr "Spitzname" -#: AKModel/models.py:169 +#: AKModel/models.py:230 msgid "Name to identify an AK owner by" msgstr "Name, durch den eine AK-Leitung identifiziert wird" -#: AKModel/models.py:170 +#: AKModel/models.py:231 msgid "Slug" msgstr "Slug" -#: AKModel/models.py:170 +#: AKModel/models.py:231 msgid "Slug for URL generation" msgstr "Slug für URL-Generierung" -#: AKModel/models.py:171 +#: AKModel/models.py:232 msgid "Institution" msgstr "Instutution" -#: AKModel/models.py:171 +#: AKModel/models.py:232 msgid "Uni etc." msgstr "Universität o.ä." -#: AKModel/models.py:172 AKModel/models.py:321 +#: AKModel/models.py:233 AKModel/models.py:398 msgid "Web Link" msgstr "Internet Link" -#: AKModel/models.py:172 +#: AKModel/models.py:233 msgid "Link to Homepage" msgstr "Link zu Homepage oder Webseite" -#: AKModel/models.py:178 AKModel/models.py:687 +#: AKModel/models.py:239 AKModel/models.py:764 msgid "AK Owner" msgstr "AK-Leitung" -#: AKModel/models.py:179 +#: AKModel/models.py:240 msgid "AK Owners" msgstr "AK-Leitungen" -#: AKModel/models.py:243 +#: AKModel/models.py:320 msgid "Name of the AK Category" msgstr "Name der AK-Kategorie" -#: AKModel/models.py:244 AKModel/models.py:268 +#: AKModel/models.py:321 AKModel/models.py:345 msgid "Color" msgstr "Farbe" -#: AKModel/models.py:244 AKModel/models.py:268 +#: AKModel/models.py:321 AKModel/models.py:345 msgid "Color for displaying" msgstr "Farbe für die Anzeige" -#: AKModel/models.py:245 AKModel/models.py:315 +#: AKModel/models.py:322 AKModel/models.py:392 msgid "Description" msgstr "Beschreibung" -#: AKModel/models.py:246 +#: AKModel/models.py:323 msgid "Short description of this AK Category" msgstr "Beschreibung der AK-Kategorie" -#: AKModel/models.py:247 +#: AKModel/models.py:324 msgid "Present by default" msgstr "Defaultmäßig präsentieren" -#: AKModel/models.py:248 +#: AKModel/models.py:325 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:333 msgid "AK Categories" msgstr "AK-Kategorien" -#: AKModel/models.py:267 +#: AKModel/models.py:344 msgid "Name of the AK Track" msgstr "Name des AK-Tracks" -#: AKModel/models.py:274 +#: AKModel/models.py:351 msgid "AK Track" msgstr "AK-Track" -#: AKModel/models.py:275 +#: AKModel/models.py:352 msgid "AK Tracks" msgstr "AK-Tracks" -#: AKModel/models.py:294 +#: AKModel/models.py:371 msgid "Name of the Requirement" msgstr "Name der Anforderung" -#: AKModel/models.py:300 AKModel/models.py:691 +#: AKModel/models.py:377 AKModel/models.py:768 msgid "AK Requirement" msgstr "AK-Anforderung" -#: AKModel/models.py:301 +#: AKModel/models.py:378 msgid "AK Requirements" msgstr "AK-Anforderungen" -#: AKModel/models.py:312 +#: AKModel/models.py:389 msgid "Name of the AK" msgstr "Name des AKs" -#: AKModel/models.py:313 +#: AKModel/models.py:390 msgid "Short Name" msgstr "Kurzer Name" -#: AKModel/models.py:314 +#: AKModel/models.py:391 msgid "Name displayed in the schedule" msgstr "Name zur Anzeige im AK-Plan" -#: AKModel/models.py:315 +#: AKModel/models.py:392 msgid "Description of the AK" msgstr "Beschreibung des AKs" -#: AKModel/models.py:317 +#: AKModel/models.py:394 msgid "Owners" msgstr "Leitungen" -#: AKModel/models.py:318 +#: AKModel/models.py:395 msgid "Those organizing the AK" msgstr "Menschen, die den AK organisieren und halten" -#: AKModel/models.py:321 +#: AKModel/models.py:398 msgid "Link to wiki page" msgstr "Link zur Wiki Seite" -#: AKModel/models.py:322 +#: AKModel/models.py:399 msgid "Protocol Link" msgstr "Protokolllink" -#: AKModel/models.py:322 +#: AKModel/models.py:399 msgid "Link to protocol" msgstr "Link zum Protokoll" -#: AKModel/models.py:324 +#: AKModel/models.py:401 msgid "Category" msgstr "Kategorie" -#: AKModel/models.py:325 +#: AKModel/models.py:402 msgid "Category of the AK" msgstr "Kategorie des AKs" -#: AKModel/models.py:326 +#: AKModel/models.py:403 msgid "Track" msgstr "Track" -#: AKModel/models.py:327 +#: AKModel/models.py:404 msgid "Track the AK belongs to" msgstr "Track zu dem der AK gehört" -#: AKModel/models.py:329 +#: AKModel/models.py:406 msgid "Resolution Intention" msgstr "Resolutionsabsicht" -#: AKModel/models.py:330 +#: AKModel/models.py:407 msgid "Intends to submit a resolution" msgstr "Beabsichtigt eine Resolution einzureichen" -#: AKModel/models.py:331 +#: AKModel/models.py:408 msgid "Present this AK" msgstr "AK präsentieren" -#: AKModel/models.py:332 +#: AKModel/models.py:409 msgid "Present results of this AK" msgstr "Die Ergebnisse dieses AKs vorstellen" -#: AKModel/models.py:334 AKModel/views/status.py:163 +#: AKModel/models.py:411 AKModel/views/status.py:170 msgid "Requirements" msgstr "Anforderungen" -#: AKModel/models.py:335 +#: AKModel/models.py:412 msgid "AK's Requirements" msgstr "Anforderungen des AKs" -#: AKModel/models.py:337 +#: AKModel/models.py:414 msgid "Conflicting AKs" msgstr "AK-Konflikte" -#: AKModel/models.py:338 +#: AKModel/models.py:415 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:416 msgid "Prerequisite AKs" msgstr "Vorausgesetzte AKs" -#: AKModel/models.py:340 +#: AKModel/models.py:417 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:419 msgid "Organizational Notes" msgstr "Notizen zur Organisation" -#: AKModel/models.py:343 +#: AKModel/models.py:420 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:423 msgid "Interest" msgstr "Interesse" -#: AKModel/models.py:346 +#: AKModel/models.py:423 msgid "Expected number of people" msgstr "Erwartete Personenzahl" -#: AKModel/models.py:347 +#: AKModel/models.py:424 msgid "Interest Counter" msgstr "Interessenszähler" -#: AKModel/models.py:348 +#: AKModel/models.py:425 msgid "People who have indicated interest online" msgstr "Anzahl Personen, die online Interesse bekundet haben" -#: AKModel/models.py:353 +#: AKModel/models.py:430 msgid "Export?" msgstr "Export?" -#: AKModel/models.py:354 +#: AKModel/models.py:431 msgid "Include AK in wiki export?" msgstr "AK bei Wiki-Export berücksichtigen?" -#: AKModel/models.py:484 +#: AKModel/models.py:561 msgid "Name or number of the room" msgstr "Name oder Nummer des Raums" -#: AKModel/models.py:485 +#: AKModel/models.py:562 msgid "Location" msgstr "Ort" -#: AKModel/models.py:486 +#: AKModel/models.py:563 msgid "Name or number of the location" msgstr "Name oder Nummer des Ortes" -#: AKModel/models.py:487 +#: AKModel/models.py:564 msgid "Capacity" msgstr "Kapazität" -#: AKModel/models.py:488 +#: AKModel/models.py:565 msgid "Maximum number of people (-1 for unlimited)." msgstr "Maximale Personenzahl (-1 wenn unbeschränkt)." -#: AKModel/models.py:489 +#: AKModel/models.py:566 msgid "Properties" msgstr "Eigenschaften" -#: AKModel/models.py:490 +#: AKModel/models.py:567 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:574 AKModel/views/status.py:59 msgid "Rooms" msgstr "Räume" -#: AKModel/models.py:520 +#: AKModel/models.py:597 msgid "AK being mapped" msgstr "AK, der zugeordnet wird" -#: AKModel/models.py:522 +#: AKModel/models.py:599 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:600 AKModel/models.py:943 msgid "Slot Begin" msgstr "Beginn des Slots" -#: AKModel/models.py:523 AKModel/models.py:866 +#: AKModel/models.py:600 AKModel/models.py:943 msgid "Time and date the slot begins" msgstr "Zeit und Datum zu der der AK beginnt" -#: AKModel/models.py:525 +#: AKModel/models.py:602 msgid "Duration" msgstr "Dauer" -#: AKModel/models.py:526 +#: AKModel/models.py:603 msgid "Length in hours" msgstr "Länge in Stunden" -#: AKModel/models.py:528 +#: AKModel/models.py:605 msgid "Scheduling fixed" msgstr "Planung fix" -#: AKModel/models.py:529 +#: AKModel/models.py:606 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:611 msgid "Last update" msgstr "Letzte Aktualisierung" -#: AKModel/models.py:537 +#: AKModel/models.py:614 msgid "AK Slot" msgstr "AK-Slot" -#: AKModel/models.py:538 AKModel/models.py:684 +#: AKModel/models.py:615 AKModel/models.py:761 msgid "AK Slots" msgstr "AK-Slot" -#: AKModel/models.py:560 AKModel/models.py:569 +#: AKModel/models.py:637 AKModel/models.py:646 msgid "Not scheduled yet" msgstr "Noch nicht geplant" -#: AKModel/models.py:617 +#: AKModel/models.py:694 msgid "AK this message belongs to" msgstr "AK zu dem die Nachricht gehört" -#: AKModel/models.py:618 +#: AKModel/models.py:695 msgid "Message text" msgstr "Nachrichtentext" -#: AKModel/models.py:619 +#: AKModel/models.py:696 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:700 msgid "Resolved" msgstr "Erledigt" -#: AKModel/models.py:624 +#: AKModel/models.py:701 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:704 msgid "AK Orga Message" msgstr "AK-Organachricht" -#: AKModel/models.py:628 +#: AKModel/models.py:705 msgid "AK Orga Messages" msgstr "AK-Organachrichten" -#: AKModel/models.py:645 +#: AKModel/models.py:722 msgid "Constraint Violation" msgstr "Constraintverletzung" -#: AKModel/models.py:646 +#: AKModel/models.py:723 msgid "Constraint Violations" msgstr "Constraintverletzungen" -#: AKModel/models.py:653 +#: AKModel/models.py:730 msgid "Owner has two parallel slots" msgstr "Leitung hat zwei Slots parallel" -#: AKModel/models.py:654 +#: AKModel/models.py:731 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:732 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:733 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:734 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:735 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:737 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:738 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:739 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:740 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:741 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:747 msgid "Warning" msgstr "Warnung" -#: AKModel/models.py:671 +#: AKModel/models.py:748 msgid "Violation" msgstr "Verletzung" -#: AKModel/models.py:673 +#: AKModel/models.py:750 msgid "Type" msgstr "Art" -#: AKModel/models.py:674 +#: AKModel/models.py:751 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:752 msgid "Level" msgstr "Level" -#: AKModel/models.py:676 +#: AKModel/models.py:753 msgid "Severity level of the violation" msgstr "Schweregrad der Verletzung" -#: AKModel/models.py:683 +#: AKModel/models.py:760 msgid "AK(s) belonging to this constraint" msgstr "AK(s), die zu diesem Constraint gehören" -#: AKModel/models.py:685 +#: AKModel/models.py:762 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:764 msgid "AK Owner belonging to this constraint" msgstr "AK Leitung(en), die zu diesem Constraint gehören" -#: AKModel/models.py:689 +#: AKModel/models.py:766 msgid "Room belonging to this constraint" msgstr "Raum, der zu diesem Constraint gehört" -#: AKModel/models.py:692 +#: AKModel/models.py:769 msgid "AK Requirement belonging to this constraint" msgstr "AK Anforderung, die zu diesem Constraint gehört" -#: AKModel/models.py:694 +#: AKModel/models.py:771 msgid "AK Category belonging to this constraint" msgstr "AK Kategorie, di zu diesem Constraint gehört" -#: AKModel/models.py:696 +#: AKModel/models.py:773 msgid "Comment" msgstr "Kommentar" -#: AKModel/models.py:696 +#: AKModel/models.py:773 msgid "Comment or further details for this violation" msgstr "Kommentar oder weitere Details zu dieser Vereletzung" -#: AKModel/models.py:699 +#: AKModel/models.py:776 msgid "Timestamp" msgstr "Timestamp" -#: AKModel/models.py:699 +#: AKModel/models.py:776 msgid "Time of creation" msgstr "Zeitpunkt der ERstellung" -#: AKModel/models.py:700 +#: AKModel/models.py:777 msgid "Manually Resolved" msgstr "Manuell behoben" -#: AKModel/models.py:701 +#: AKModel/models.py:778 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:805 #: AKModel/templates/admin/AKModel/requirements_overview.html:27 msgid "Details" msgstr "Details" -#: AKModel/models.py:862 +#: AKModel/models.py:939 msgid "Default Slot" msgstr "Standardslot" -#: AKModel/models.py:867 +#: AKModel/models.py:944 msgid "Slot End" msgstr "Ende des Slots" -#: AKModel/models.py:867 +#: AKModel/models.py:944 msgid "Time and date the slot ends" msgstr "Zeit und Datum zu der der Slot endet" -#: AKModel/models.py:872 +#: AKModel/models.py:949 msgid "Primary categories" msgstr "Primäre Kategorien" -#: AKModel/models.py:873 +#: AKModel/models.py:950 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:20 msgid "Requirements for Event" msgstr "Anforderungen für das Event" -#: AKModel/views/ak.py:34 +#: AKModel/views/ak.py:37 msgid "AK CSV Export" msgstr "AK-CSV-Export" -#: AKModel/views/ak.py:48 +#: AKModel/views/ak.py:50 +msgid "AK JSON Export" +msgstr "AK-JSON-Export" + +#: AKModel/views/ak.py:133 msgid "AK Wiki Export" msgstr "AK-Wiki-Export" -#: AKModel/views/ak.py:59 AKModel/views/manage.py:53 +#: AKModel/views/ak.py:144 AKModel/views/manage.py:53 msgid "Wishes" msgstr "Wünsche" -#: AKModel/views/ak.py:71 +#: AKModel/views/ak.py:156 msgid "Delete AK Orga Messages" msgstr "AK-Organachrichten löschen" -#: AKModel/views/ak.py:89 +#: AKModel/views/ak.py:174 msgid "AK Orga Messages successfully deleted" msgstr "AK-Organachrichten erfolgreich gelöscht" -#: AKModel/views/ak.py:101 +#: AKModel/views/ak.py:186 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:187 msgid "Reset of interest in AKs successful." msgstr "Interesse an AKs erfolgreich zurückgesetzt." -#: AKModel/views/ak.py:116 +#: AKModel/views/ak.py:201 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:202 msgid "AKs' interest counters set back to 0." msgstr "Interessenszähler der AKs zurückgesetzt" @@ -1173,7 +1177,7 @@ 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:35 AKModel/views/status.py:153 msgid "Export AK Slides" msgstr "AK-Folien exportieren" @@ -1241,7 +1245,7 @@ msgstr "Den Plan/die Pläne verbergen von:" msgid "Plan unpublished" msgstr "Plan verborgen" -#: AKModel/views/manage.py:166 AKModel/views/status.py:130 +#: AKModel/views/manage.py:166 AKModel/views/status.py:129 msgid "Edit Default Slots" msgstr "Standardslots bearbeiten" @@ -1257,12 +1261,16 @@ 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:252 +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/AKPlan/locale/de_DE/LC_MESSAGES/django.po b/AKPlan/locale/de_DE/LC_MESSAGES/django.po index 90ee78c1..1bbfe725 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 3f47223f..68529ec4 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/AKSubmission/locale/de_DE/LC_MESSAGES/django.po b/AKSubmission/locale/de_DE/LC_MESSAGES/django.po index b25db760..e8add44b 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" -- GitLab From 6884a49bc4349fb2c3ff9866b2e701bca64a6788 Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Mon, 27 May 2024 05:14:56 +0200 Subject: [PATCH 03/77] Fix migration erros --- AKModel/models.py | 52 +++++++++++++++++++++++++++++++-------------- AKModel/views/ak.py | 4 +--- 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/AKModel/models.py b/AKModel/models.py index c4c6f804..7049fca3 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -297,22 +297,6 @@ class AKOwner(models.Model): """ return AKOwner.objects.get(event=event, slug=slug) - def as_json(self) -> str: - data = { - "id": self.pk, - "info": { - "name": self.name, - }, - "capacity": self.capacity, - "fulfilled_room_constraints": [constraint.name - for constraint in self.properties.all()], - "time_constraints": [f"availability-room-{self.pk}"] - } - - return json.dumps(data) - - - class AKCategory(models.Model): """ An AKCategory describes the characteristics of an AK, e.g. content vs. recreational. @@ -590,6 +574,20 @@ class Room(models.Model): def __str__(self): return self.title + def as_json(self) -> str: + data = { + "id": self.pk, + "info": { + "name": self.name, + }, + "capacity": self.capacity, + "fulfilled_room_constraints": [constraint.name + for constraint in self.properties.all()], + "time_constraints": [f"availability-room-{self.pk}"] + } + + return json.dumps(data) + class AKSlot(models.Model): """ An AK Mapping matches an AK to a room during a certain time. @@ -685,6 +683,28 @@ 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: + data = { + "id": 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"].append(f"availability-ak-{self.pk}") + data["time_constraints"] += [f"availability-person-{owner.pk}" + for owner in self.ak.owners.all()] + + return json.dumps(data) class AKOrgaMessage(models.Model): """ diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index 547f3606..88fc6bf4 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -8,7 +8,7 @@ from django.views.generic import ListView, DetailView from AKModel.metaviews.admin import AdminViewMixin, FilterByEventSlugMixin, EventSlugMixin, IntermediateAdminView, \ IntermediateAdminActionView -from AKModel.models import AKRequirement, AKSlot, Event, AKOrgaMessage, AK +from AKModel.models import AKRequirement, AKSlot, Event, AKOrgaMessage, AK, Room, AKOwner class AKRequirementOverview(AdminViewMixin, FilterByEventSlugMixin, ListView): @@ -81,8 +81,6 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): for person in AKOwner.objects.filter(event=self.event) for availability in person.availabilities.all()} - - for block in self.event.time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR): current_block = [] -- GitLab From ce126f7050ce34c33b24e137f3710a343a41db77 Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Mon, 27 May 2024 05:15:09 +0200 Subject: [PATCH 04/77] Handle case of unset reso_deadline --- AKModel/views/ak.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index 88fc6bf4..0539898e 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -89,7 +89,7 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): slots_in_an_hour=SLOTS_IN_AN_HOUR) constraints = [] - if slot.end < self.event.reso_deadline: + if self.event.reso_deadline is None or slot.end < self.event.reso_deadline: constraints.append("resolution") for (ak, availability) in ak_availabilities.items(): -- GitLab From ae5cfae376066b3753446ec040085e2d171a2b5e Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Mon, 27 May 2024 06:52:44 +0200 Subject: [PATCH 05/77] Do not introduce constraints if available for whole event --- AKModel/models.py | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/AKModel/models.py b/AKModel/models.py index 7049fca3..55060197 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -575,6 +575,17 @@ class Room(models.Model): return self.title def as_json(self) -> str: + from AKModel.availability.models import Availability + + # check if room is available for the whole event + # -> no time constraint needs to be introduced + full_event = Availability(event=self.event, start=self.event.start, end=self.event.end) + avail_union = Availability.union(self.availabilities.all()) + if len(avail_union) == 1 and avail_union[0].contains(full_event): + time_constraints = [] + else: + time_constraints = [f"availability-room-{self.pk}"] + data = { "id": self.pk, "info": { @@ -583,7 +594,7 @@ class Room(models.Model): "capacity": self.capacity, "fulfilled_room_constraints": [constraint.name for constraint in self.properties.all()], - "time_constraints": [f"availability-room-{self.pk}"] + "time_constraints": time_constraints } return json.dumps(data) @@ -684,6 +695,25 @@ class AKSlot(models.Model): super().save(force_insert, force_update, using, update_fields) def as_json(self) -> str: + from AKModel.availability.models import Availability + + # check if ak resp. owner is available for the whole event + # -> no time constraint needs to be introduced + full_event = Availability(event=self.event, start=self.event.start, end=self.event.end) + + ak_avail_union = Availability.union(self.ak.availabilities.all()) + if not ak_avail_union or ak_avail_union[0].contains(full_event): + ak_time_constraints = [] + else: + ak_time_constraints = [f"availability-ak-{self.ak.pk}"] + + def _owner_time_constraints(owner: AKOwner): + owner_avail_union = Availability.union(owner.availabilities.all()) + if not owner_avail_union or owner_avail_union[0].contains(full_event): + return [] + else: + return [f"availability-person-{owner.pk}"] + data = { "id": self.pk, "duration": int(self.duration * self.slots_in_an_hour), @@ -700,9 +730,9 @@ class AKSlot(models.Model): }, } - data["time_constraints"].append(f"availability-ak-{self.pk}") - data["time_constraints"] += [f"availability-person-{owner.pk}" - for owner in self.ak.owners.all()] + data["time_constraints"].extend(ak_time_constraints) + for owner in self.ak.owners.all(): + data["time_constraints"].extend(_owner_time_constraints(owner)) return json.dumps(data) -- GitLab From 9db6b5b7d3e31c00588f5a5ab5cf363cc09f64ff Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Mon, 27 May 2024 07:36:57 +0200 Subject: [PATCH 06/77] Add note for possible bug or unexpected behaviour --- AKModel/models.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/AKModel/models.py b/AKModel/models.py index 55060197..68158954 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -579,6 +579,9 @@ class Room(models.Model): # check if room is available for the whole event # -> no time constraint needs to be introduced + + # NOTE: Cannot use `Availability.with_event_length` as its end is the + # event end + 1 day full_event = Availability(event=self.event, start=self.event.start, end=self.event.end) avail_union = Availability.union(self.availabilities.all()) if len(avail_union) == 1 and avail_union[0].contains(full_event): @@ -699,6 +702,9 @@ class AKSlot(models.Model): # check if ak resp. owner is available for the whole event # -> no time constraint needs to be introduced + + # NOTE: Cannot use `Availability.with_event_length` as its end is the + # event end + 1 day full_event = Availability(event=self.event, start=self.event.start, end=self.event.end) ak_avail_union = Availability.union(self.ak.availabilities.all()) -- GitLab From e2884a5911f48baf89486f3b32c589351ce34149 Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Mon, 27 May 2024 07:37:17 +0200 Subject: [PATCH 07/77] Fix check --- AKModel/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AKModel/models.py b/AKModel/models.py index 68158954..c0078013 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -584,7 +584,7 @@ class Room(models.Model): # event end + 1 day full_event = Availability(event=self.event, start=self.event.start, end=self.event.end) avail_union = Availability.union(self.availabilities.all()) - if len(avail_union) == 1 and avail_union[0].contains(full_event): + if not avail_union or avail_union[0].contains(full_event): time_constraints = [] else: time_constraints = [f"availability-room-{self.pk}"] -- GitLab From 440709c1891512885031e45c073817e3a8f1b37c Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Mon, 27 May 2024 07:46:06 +0200 Subject: [PATCH 08/77] Introduce availabilits class method is_event_covered --- AKModel/availability/models.py | 8 ++++++++ AKModel/models.py | 17 +++-------------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/AKModel/availability/models.py b/AKModel/availability/models.py index 7ce794dc..de51e438 100644 --- a/AKModel/availability/models.py +++ b/AKModel/availability/models.py @@ -267,6 +267,14 @@ 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, availabilities: List['Availability']) -> 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/models.py b/AKModel/models.py index c0078013..2052a5cd 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -579,12 +579,7 @@ class Room(models.Model): # check if room is available for the whole event # -> no time constraint needs to be introduced - - # NOTE: Cannot use `Availability.with_event_length` as its end is the - # event end + 1 day - full_event = Availability(event=self.event, start=self.event.start, end=self.event.end) - avail_union = Availability.union(self.availabilities.all()) - if not avail_union or avail_union[0].contains(full_event): + if Availability.is_event_covered(self.event, self.availabilities.all()): time_constraints = [] else: time_constraints = [f"availability-room-{self.pk}"] @@ -703,19 +698,13 @@ class AKSlot(models.Model): # check if ak resp. owner is available for the whole event # -> no time constraint needs to be introduced - # NOTE: Cannot use `Availability.with_event_length` as its end is the - # event end + 1 day - full_event = Availability(event=self.event, start=self.event.start, end=self.event.end) - - ak_avail_union = Availability.union(self.ak.availabilities.all()) - if not ak_avail_union or ak_avail_union[0].contains(full_event): + if 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): - owner_avail_union = Availability.union(owner.availabilities.all()) - if not owner_avail_union or owner_avail_union[0].contains(full_event): + if Availability.is_event_covered(self.event, owner.availabilities.all()): return [] else: return [f"availability-person-{owner.pk}"] -- GitLab From 32e7ee941e55202981a95ed0c05540188449ed15 Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Mon, 27 May 2024 07:54:03 +0200 Subject: [PATCH 09/77] Filter unnecessary fulfilled constraints --- AKModel/views/ak.py | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index 0539898e..2c09ca08 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -1,5 +1,6 @@ import json from datetime import timedelta +from typing import List from django.contrib import messages from django.urls import reverse_lazy @@ -71,15 +72,24 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): for slot in context["slots"]: slot.slots_in_an_hour = SLOTS_IN_AN_HOUR - ak_availabilities = {slot.ak.pk: availability - for slot in context["slots"] - for availability in slot.ak.availabilities.all()} - room_availabilities = {room.pk: availability - for room in rooms - for availability in room.availabilities.all()} - person_availabilities = {person.pk: availability - for person in AKOwner.objects.filter(event=self.event) - for availability in person.availabilities.all()} + 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) + } + + def _test_add_constraint(slot: Availability, availabilities: List[Availability]) -> bool: + return ( + (not Availability.is_event_covered(self.event, availabilities)) + and any(availability.contains(slot) for availability in availabilities) + ) for block in self.event.time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR): current_block = [] @@ -92,16 +102,16 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): if self.event.reso_deadline is None or slot.end < self.event.reso_deadline: constraints.append("resolution") - for (ak, availability) in ak_availabilities.items(): - if availability.contains(slot): + for ak, availabilities in ak_availabilities.items(): + if _test_add_constraint(slot, availabilities): constraints.append(f"availability-ak-{ak}") - for (person, availability) in person_availabilities.items(): - if availability.contains(slot): + for person, availabilities in person_availabilities.items(): + if _test_add_constraint(slot, availabilities): constraints.append(f"availability-person-{person}") - for (room, availability) in room_availabilities.items(): - if availability.contains(slot): + for person, availabilities in room_availabilities.items(): + if _test_add_constraint(slot, availabilities): constraints.append(f"availability-room-{room}") current_block.append({ -- GitLab From 20ab6461f233093004b458c64acec602a51e8de6 Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Mon, 27 May 2024 09:03:20 +0200 Subject: [PATCH 10/77] Handle fixed AKs --- AKModel/models.py | 2 +- AKModel/views/ak.py | 26 +++++++++++++++++++++----- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/AKModel/models.py b/AKModel/models.py index 2052a5cd..bfa31082 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -698,7 +698,7 @@ class AKSlot(models.Model): # check if ak resp. owner is available for the whole event # -> no time constraint needs to be introduced - if Availability.is_event_covered(self.event, self.ak.availabilities.all()): + 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}"] diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index 2c09ca08..d3b0e93c 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -85,11 +85,27 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): for person in AKOwner.objects.filter(event=self.event) } + ak_fixed = { + ak: values.get() + for ak in ak_availabilities.keys() + if (values := AKSlot.objects.select_related().filter(ak__pk=ak, fixed=True)).exists() + } + + def _test_slot_contained(slot: Availability, availabilities: List[Availability]) -> bool: + return any(availability.contains(slot) for availability in availabilities) + + def _test_event_covered(slot: Availability, availabilities: List[Availability]) -> bool: + return not Availability.is_event_covered(self.event, availabilities) + + def _test_fixed_ak(ak, slot) -> bool: + if not ak in ak_fixed: + return False + + fixed_slot = Availability(self.event, start=ak_fixed[ak].start, end=ak_fixed[ak].end) + return fixed_slot.overlaps(slot, strict=True) + def _test_add_constraint(slot: Availability, availabilities: List[Availability]) -> bool: - return ( - (not Availability.is_event_covered(self.event, availabilities)) - and any(availability.contains(slot) for availability in availabilities) - ) + return _test_event_covered(slot, availabilities) and _test_slot_contained(slot, availabilities) for block in self.event.time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR): current_block = [] @@ -103,7 +119,7 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): constraints.append("resolution") for ak, availabilities in ak_availabilities.items(): - if _test_add_constraint(slot, availabilities): + if _test_add_constraint(slot, availabilities) or _test_fixed_ak(ak, slot): constraints.append(f"availability-ak-{ak}") for person, availabilities in person_availabilities.items(): -- GitLab From e6b791af0ba8a5a7dd5ec97536e68c8e0faedb13 Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Mon, 27 May 2024 09:09:11 +0200 Subject: [PATCH 11/77] Handle fixed rooms --- AKModel/models.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/AKModel/models.py b/AKModel/models.py index bfa31082..fe8bb843 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -595,6 +595,8 @@ class Room(models.Model): "time_constraints": time_constraints } + data["fulfilled_room_constraints"].append(f"availability-room-{self.pk}") + return json.dumps(data) @@ -729,6 +731,9 @@ class AKSlot(models.Model): for owner in self.ak.owners.all(): data["time_constraints"].extend(_owner_time_constraints(owner)) + if self.room is not None: + data["room_constraints"].append(f"availability-room-{self.room.pk}") + return json.dumps(data) class AKOrgaMessage(models.Model): -- GitLab From 2b9419cebd54f3862f0015c8699ee646c527d4be Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Mon, 27 May 2024 09:16:35 +0200 Subject: [PATCH 12/77] type hints --- AKModel/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AKModel/models.py b/AKModel/models.py index fe8bb843..4cc9343d 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -196,7 +196,7 @@ class Event(models.Model): yield current_block - def time_slot(self, *, time_slot_index, slots_in_an_hour=1.0): + def time_slot(self, *, time_slot_index: int, slots_in_an_hour: float = 1.0) -> "Availability": from AKModel.availability.models import Availability slot_duration = timedelta(hours=(1.0 / slots_in_an_hour)) @@ -206,7 +206,7 @@ class Event(models.Model): start=start, end=start + slot_duration) - def schedule_from_json(self, schedule): + def schedule_from_json(self, schedule: str) -> None: schedule = json.loads(schedule) slots_in_an_hour = schedule["input"]["timeslots"]["info"]["duration"] -- GitLab From 034748fecf5dca5f2bde21a4583fe1b29bb7ba92 Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Mon, 27 May 2024 09:28:49 +0200 Subject: [PATCH 13/77] Fix bug that assigned time but not assigned room crashes --- AKScheduling/models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AKScheduling/models.py b/AKScheduling/models.py index 1495f311..aa6be3d0 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(): -- GitLab From dabd1769bc9ea3c54c1a4194da85896a1fcb743d Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Tue, 28 May 2024 21:30:27 +0200 Subject: [PATCH 14/77] Fix import code --- AKModel/models.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/AKModel/models.py b/AKModel/models.py index 4cc9343d..5337bb4f 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -212,17 +212,19 @@ class Event(models.Model): slots_in_an_hour = schedule["input"]["timeslots"]["info"]["duration"] for scheduled_slot in schedule["scheduled_aks"]: - slot = AKSlot.objects.get(scheduled_slot["ak_id"]) - slot.room = scheduled_slot["room_id"] + slot = AKSlot.objects.get(id=int(scheduled_slot["ak_id"])) + slot.room = Room.objects.get(id=int(scheduled_slot["room_id"])) - start = min(scheduled_slot["time_slot_ids"]) - end = max(scheduled_slot["time_slot_ids"]) + scheduled_slot["timeslot_ids"] = list(map(int, scheduled_slot["timeslot_ids"])) + + start = min(scheduled_slot["timeslot_ids"]) + end = max(scheduled_slot["timeslot_ids"]) slot.start = self.time_slot(time_slot_index=start, - slots_in_an_hour=slots_in_an_hour) - slot.end = self.time_slot(time_slot_index=end + 1, - slots_in_an_hour=slots_in_an_hour) + slots_in_an_hour=slots_in_an_hour).start + slot.duration = (end - start + 1) * timedelta(hours=(1.0 / slots_in_an_hour)).total_seconds() / 3600.0 + slot.save() class AKOwner(models.Model): """ An AKOwner describes the person organizing/holding an AK. -- GitLab From cebd106214a660d411de2fcbf775174e13448d95 Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Tue, 28 May 2024 21:30:37 +0200 Subject: [PATCH 15/77] Add missing import --- AKModel/views/manage.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AKModel/views/manage.py b/AKModel/views/manage.py index 0ed33044..f4564f09 100644 --- a/AKModel/views/manage.py +++ b/AKModel/views/manage.py @@ -4,8 +4,10 @@ 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 -- GitLab From 6df70c288f04c52187298b94b0ed20a3e8a5d55b Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Wed, 29 May 2024 01:14:59 +0200 Subject: [PATCH 16/77] Export ids as str --- AKModel/models.py | 4 ++-- AKModel/views/ak.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/AKModel/models.py b/AKModel/models.py index 5337bb4f..8e221a3a 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -587,7 +587,7 @@ class Room(models.Model): time_constraints = [f"availability-room-{self.pk}"] data = { - "id": self.pk, + "id": str(self.pk), "info": { "name": self.name, }, @@ -714,7 +714,7 @@ class AKSlot(models.Model): return [f"availability-person-{owner.pk}"] data = { - "id": self.pk, + "id": str(self.pk), "duration": int(self.duration * self.slots_in_an_hour), "properties": {}, "room_constraints": [constraint.name diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index d3b0e93c..b8b55ee8 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -131,7 +131,7 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): constraints.append(f"availability-room-{room}") current_block.append({ - "id": slot_index, + "id": str(slot_index), "info": { "start": slot.simplified, }, -- GitLab From b6c5952e618973bf2a4be22cc6d869696915e3a8 Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Wed, 29 May 2024 18:34:11 +0200 Subject: [PATCH 17/77] Refactor --- AKModel/views/ak.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index b8b55ee8..611cd619 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -86,9 +86,9 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): } ak_fixed = { - ak: values.get() - for ak in ak_availabilities.keys() - if (values := AKSlot.objects.select_related().filter(ak__pk=ak, fixed=True)).exists() + 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() } def _test_slot_contained(slot: Availability, availabilities: List[Availability]) -> bool: @@ -97,11 +97,11 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): def _test_event_covered(slot: Availability, availabilities: List[Availability]) -> bool: return not Availability.is_event_covered(self.event, availabilities) - def _test_fixed_ak(ak, slot) -> bool: - if not ak in ak_fixed: + def _test_fixed_ak(ak_id, slot: Availability) -> bool: + if not ak_id in ak_fixed: return False - fixed_slot = Availability(self.event, start=ak_fixed[ak].start, end=ak_fixed[ak].end) + 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(slot: Availability, availabilities: List[Availability]) -> bool: @@ -118,17 +118,17 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): if self.event.reso_deadline is None or slot.end < self.event.reso_deadline: constraints.append("resolution") - for ak, availabilities in ak_availabilities.items(): - if _test_add_constraint(slot, availabilities) or _test_fixed_ak(ak, slot): - constraints.append(f"availability-ak-{ak}") + for ak_id, availabilities in ak_availabilities.items(): + if _test_add_constraint(slot, availabilities) or _test_fixed_ak(ak_id, slot): + constraints.append(f"availability-ak-{ak_id}") - for person, availabilities in person_availabilities.items(): + for person_id, availabilities in person_availabilities.items(): if _test_add_constraint(slot, availabilities): - constraints.append(f"availability-person-{person}") + constraints.append(f"availability-person-{person_id}") - for person, availabilities in room_availabilities.items(): + for room_id, availabilities in room_availabilities.items(): if _test_add_constraint(slot, availabilities): - constraints.append(f"availability-room-{room}") + constraints.append(f"availability-room-{room_id}") current_block.append({ "id": str(slot_index), -- GitLab From d2f88dd144b85620f2a6bb34af4479965e383734 Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Wed, 29 May 2024 18:53:32 +0200 Subject: [PATCH 18/77] More refactoring --- AKModel/views/ak.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index 611cd619..a8e07d26 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -113,29 +113,33 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): for slot_index in block: slot = self.event.time_slot(time_slot_index=slot_index, slots_in_an_hour=SLOTS_IN_AN_HOUR) - constraints = [] + time_constraints = [] if self.event.reso_deadline is None or slot.end < self.event.reso_deadline: - constraints.append("resolution") - - for ak_id, availabilities in ak_availabilities.items(): - if _test_add_constraint(slot, availabilities) or _test_fixed_ak(ak_id, slot): - constraints.append(f"availability-ak-{ak_id}") - - for person_id, availabilities in person_availabilities.items(): - if _test_add_constraint(slot, availabilities): - constraints.append(f"availability-person-{person_id}") - - for room_id, availabilities in room_availabilities.items(): - if _test_add_constraint(slot, availabilities): - constraints.append(f"availability-room-{room_id}") + time_constraints.append("resolution") + + time_constraints.extend([ + f"availability-ak-{ak_id}" + for ak_id, availabilities in ak_availabilities.items() + if _test_add_constraint(slot, availabilities) or _test_fixed_ak(ak_id, slot) + ]) + time_constraints.extend([ + f"availability-person-{person_id}" + for person_id, availabilities in person_availabilities.items() + if _test_add_constraint(slot, availabilities) + ]) + time_constraints.extend([ + f"availability-room-{room_id}" + for room_id, availabilities in room_availabilities.items() + if _test_add_constraint(slot, availabilities) + ]) current_block.append({ "id": str(slot_index), "info": { "start": slot.simplified, }, - "fulfilled_time_constraints": constraints, + "fulfilled_time_constraints": time_constraints, }) timeslots["blocks"].append(current_block) -- GitLab From 14c3690a7b99af8156974daf06d6118c9f7bd5f0 Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Wed, 29 May 2024 18:54:15 +0200 Subject: [PATCH 19/77] Add no-proxy constraints --- AKModel/models.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/AKModel/models.py b/AKModel/models.py index 8e221a3a..3f5a0f4c 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -599,6 +599,9 @@ class Room(models.Model): 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) @@ -736,6 +739,9 @@ class AKSlot(models.Model): if self.room is not None: 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): -- GitLab From d92c87e1e4d0d6fa74f6941c559fec047499fd5c Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Wed, 29 May 2024 19:14:06 +0200 Subject: [PATCH 20/77] Add empty info dict to json export --- AKModel/templates/admin/AKModel/ak_json_export.html | 3 ++- AKModel/views/ak.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/AKModel/templates/admin/AKModel/ak_json_export.html b/AKModel/templates/admin/AKModel/ak_json_export.html index da582504..38e5526e 100644 --- a/AKModel/templates/admin/AKModel/ak_json_export.html +++ b/AKModel/templates/admin/AKModel/ak_json_export.html @@ -13,7 +13,8 @@ {% endif %}{% endfor %} ], "participants": {{ participants }}, - "timeslots": {{ timeslots }} + "timeslots": {{ timeslots }}, + "info": {{ info_dict }} } </pre> {% endblock %} diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index a8e07d26..90c3ef6e 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -146,6 +146,8 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): context["timeslots"] = json.dumps(timeslots) + context["info_dict"] = {} + return context -- GitLab From aea39c3c8739151b228b56c0d20ecf7f77c4779c Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Wed, 29 May 2024 19:51:19 +0200 Subject: [PATCH 21/77] Only export preset room if ak is fixed --- AKModel/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AKModel/models.py b/AKModel/models.py index 3f5a0f4c..86fa8e15 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -736,7 +736,7 @@ class AKSlot(models.Model): for owner in self.ak.owners.all(): data["time_constraints"].extend(_owner_time_constraints(owner)) - if self.room is not None: + 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"]): -- GitLab From 15f238b039f72054a1d68c921b4e7827b44800fc Mon Sep 17 00:00:00 2001 From: Felix Blanke <s6feblan@uni-bonn.de> Date: Wed, 29 May 2024 21:51:29 +0000 Subject: [PATCH 22/77] Generate timeslots based on default slots --- AKModel/models.py | 97 ++++++++++++++++++++++++++++----------------- AKModel/views/ak.py | 6 +-- 2 files changed, 63 insertions(+), 40 deletions(-) diff --git a/AKModel/models.py b/AKModel/models.py index 86fa8e15..3a91facb 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -1,6 +1,7 @@ import itertools import json -from datetime import timedelta +from datetime import datetime, timedelta +from typing import Iterable from django.db import models from django.apps import apps @@ -163,67 +164,91 @@ class Event(models.Model): .filter(availabilities__count=0, owners__count__gt=0) ) - def time_slots(self, *, slots_in_an_hour=1.0): + def _generate_slots_from_block( + self, start: datetime, end: datetime, slot_duration: timedelta, slot_index: int = 0 + ) -> Iterable[list[int, "Availability"]]: from AKModel.availability.models import Availability - rooms = Room.objects.filter(event=self) - slot_duration = timedelta(hours=(1.0 / slots_in_an_hour)) - slot_index = 0 - current_slot = self.start - current_block = [] - previous_slot = None - - room_availabilities = list({availability - for room in rooms - for availability in room.availabilities.all()}) + current_slot_start = start + previous_slot_start: datetime | None = None - while current_slot < self.end: - slot = Availability(event=self, - start=current_slot, - end=current_slot + slot_duration) + current_block = [] - if any((availability.contains(slot) - for availability in room_availabilities)): - if previous_slot is not None and previous_slot + slot_duration < current_slot: + 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(slot_index) - previous_slot = current_slot + current_block.append((slot_index, slot)) + previous_slot_start = current_slot_start slot_index += 1 - current_slot += slot_duration + current_slot_start += slot_duration - yield current_block + if current_block: + yield current_block - def time_slot(self, *, time_slot_index: int, slots_in_an_hour: float = 1.0) -> "Availability": - from AKModel.availability.models import Availability - slot_duration = timedelta(hours=(1.0 / slots_in_an_hour)) + return slot_index - start = self.start + time_slot_index * slot_duration + def uniform_time_slots(self, *, slots_in_an_hour=1.0) -> Iterable[list[int, "Availability"]]: + yield from self._generate_slots_from_block( + start=self.start, + end=self.end, + slot_duration=timedelta(hours=(1.0 / slots_in_an_hour)), + ) - return Availability(event=self, - start=start, - end=start + slot_duration) + def default_time_slots(self, *, slots_in_an_hour=1.0) -> Iterable[list[int, "Availability"]]: + 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"): + # NOTE: We do not differentiate between different primary categories + 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, + ) def schedule_from_json(self, schedule: str) -> None: schedule = json.loads(schedule) slots_in_an_hour = schedule["input"]["timeslots"]["info"]["duration"] + timeslot_dict = { + slot_idx: slot + for block in self.default_time_slots(slots_in_an_hour=slots_in_an_hour) + for slot_idx, slot 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 = min(scheduled_slot["timeslot_ids"]) - end = max(scheduled_slot["timeslot_ids"]) - - slot.start = self.time_slot(time_slot_index=start, - slots_in_an_hour=slots_in_an_hour).start + start_timeslot = timeslot_dict[min(scheduled_slot["timeslot_ids"])] + end_timeslot = timeslot_dict[max(scheduled_slot["timeslot_ids"])] - slot.duration = (end - start + 1) * timedelta(hours=(1.0 / slots_in_an_hour)).total_seconds() / 3600.0 + slot.start = start_timeslot.start + slot.duration = (end_timeslot.end - start_timeslot.start).total_seconds() / 3600.0 slot.save() class AKOwner(models.Model): diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index 90c3ef6e..c65ae266 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -107,12 +107,10 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): def _test_add_constraint(slot: Availability, availabilities: List[Availability]) -> bool: return _test_event_covered(slot, availabilities) and _test_slot_contained(slot, availabilities) - for block in self.event.time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR): + for block in self.event.default_time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR): current_block = [] - for slot_index in block: - slot = self.event.time_slot(time_slot_index=slot_index, - slots_in_an_hour=SLOTS_IN_AN_HOUR) + for slot_index, slot in block: time_constraints = [] if self.event.reso_deadline is None or slot.end < self.event.reso_deadline: -- GitLab From 77f4dac93f4f8f84417ad16c311f37c8eebcce4f Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Thu, 30 May 2024 21:27:27 +0200 Subject: [PATCH 23/77] Add more event info to json export --- AKModel/views/ak.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index c65ae266..21a6fd72 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -144,7 +144,15 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): context["timeslots"] = json.dumps(timeslots) - context["info_dict"] = {} + 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 -- GitLab From e5fa470d49c2b2fe60210a510b72a3292d2b13bc Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Fri, 31 May 2024 00:50:21 +0200 Subject: [PATCH 24/77] Adress pylint warnings --- AKModel/availability/models.py | 18 ++++++++- AKModel/forms.py | 1 + AKModel/models.py | 70 +++++++++++++++++++++++++++++++--- AKModel/views/ak.py | 60 ++++++++++++++++------------- AKModel/views/manage.py | 5 ++- 5 files changed, 119 insertions(+), 35 deletions(-) diff --git a/AKModel/availability/models.py b/AKModel/availability/models.py index de51e438..80546543 100644 --- a/AKModel/availability/models.py +++ b/AKModel/availability/models.py @@ -247,7 +247,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. @@ -268,7 +275,14 @@ class Availability(models.Model): room=room, ak=ak, ak_category=ak_category) @classmethod - def is_event_covered(cls, event, availabilities: List['Availability']) -> bool: + 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) diff --git a/AKModel/forms.py b/AKModel/forms.py index f74dc39e..74ca1b68 100644 --- a/AKModel/forms.py +++ b/AKModel/forms.py @@ -275,6 +275,7 @@ class RoomFormWithAvailabilities(AvailabilitiesFormMixin, RoomForm): class JSONImportForm(AdminIntermediateForm): + """Form to import an AK schedule from a json file.""" json_data = forms.CharField( required=True, widget=forms.Textarea, diff --git a/AKModel/models.py b/AKModel/models.py index 3a91facb..49159427 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -8,7 +8,6 @@ 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 @@ -167,6 +166,24 @@ class Event(models.Model): def _generate_slots_from_block( self, start: datetime, end: datetime, slot_duration: timedelta, slot_index: int = 0 ) -> Iterable[list[int, "Availability"]]: + """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 tuples, each consisisting of the timeslot id + and its availability to indicate its start and duration. + """ + # local import to prevent cyclic import + # pylint: disable=import-outside-toplevel from AKModel.availability.models import Availability current_slot_start = start @@ -208,14 +225,32 @@ class Event(models.Model): return slot_index def uniform_time_slots(self, *, slots_in_an_hour=1.0) -> Iterable[list[int, "Availability"]]: + """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 tuples, each consisisting of the timeslot id + and its availability to indicate its start and duration. + """ yield from self._generate_slots_from_block( start=self.start, end=self.end, - slot_duration=timedelta(hours=(1.0 / slots_in_an_hour)), + slot_duration=timedelta(hours=1.0 / slots_in_an_hour), ) def default_time_slots(self, *, slots_in_an_hour=1.0) -> Iterable[list[int, "Availability"]]: - slot_duration = timedelta(hours=(1.0 / slots_in_an_hour)) + """Discretize the all default slots into a 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 tuples, each consisisting of the timeslot id + and its availability to indicate its start and duration. + """ + 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"): @@ -228,6 +263,13 @@ class Event(models.Model): ) 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"] @@ -602,6 +644,15 @@ class Room(models.Model): 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 @@ -725,6 +776,15 @@ class AKSlot(models.Model): 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 @@ -738,9 +798,9 @@ class AKSlot(models.Model): def _owner_time_constraints(owner: AKOwner): if Availability.is_event_covered(self.event, owner.availabilities.all()): return [] - else: - return [f"availability-person-{owner.pk}"] + 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), diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index 21a6fd72..818f15d9 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -1,5 +1,4 @@ import json -from datetime import timedelta from typing import List from django.contrib import messages @@ -7,6 +6,7 @@ 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, Room, AKOwner @@ -50,25 +50,44 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): 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): - from AKModel.availability.models import Availability + 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 - rooms = Room.objects.filter(event=self.event) - participants = [] timeslots = { "info": {"duration": (1.0 / SLOTS_IN_AN_HOUR), }, "blocks": [], } - context = super().get_context_data(**kwargs) - context["rooms"] = rooms - context["participants"] = json.dumps(participants) - for slot in context["slots"]: slot.slots_in_an_hour = SLOTS_IN_AN_HOUR @@ -91,22 +110,6 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): if (values := AKSlot.objects.select_related().filter(ak__pk=ak_id, fixed=True)).exists() } - def _test_slot_contained(slot: Availability, availabilities: List[Availability]) -> bool: - return any(availability.contains(slot) for availability in availabilities) - - def _test_event_covered(slot: Availability, availabilities: List[Availability]) -> bool: - return not Availability.is_event_covered(self.event, availabilities) - - def _test_fixed_ak(ak_id, slot: Availability) -> 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(slot: Availability, availabilities: List[Availability]) -> bool: - return _test_event_covered(slot, availabilities) and _test_slot_contained(slot, availabilities) - for block in self.event.default_time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR): current_block = [] @@ -119,17 +122,20 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): time_constraints.extend([ f"availability-ak-{ak_id}" for ak_id, availabilities in ak_availabilities.items() - if _test_add_constraint(slot, availabilities) or _test_fixed_ak(ak_id, slot) + if ( + self._test_add_constraint(slot, availabilities) + or self._test_fixed_ak(ak_id, slot, ak_fixed) + ) ]) time_constraints.extend([ f"availability-person-{person_id}" for person_id, availabilities in person_availabilities.items() - if _test_add_constraint(slot, availabilities) + if self._test_add_constraint(slot, availabilities) ]) time_constraints.extend([ f"availability-room-{room_id}" for room_id, availabilities in room_availabilities.items() - if _test_add_constraint(slot, availabilities) + if self._test_add_constraint(slot, availabilities) ]) current_block.append({ diff --git a/AKModel/views/manage.py b/AKModel/views/manage.py index f4564f09..ec5076fb 100644 --- a/AKModel/views/manage.py +++ b/AKModel/views/manage.py @@ -60,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) @@ -250,6 +250,9 @@ class AKsByUserView(AdminViewMixin, EventSlugMixin, DetailView): 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") -- GitLab From 216eecff41372f2ec9375491b680ff87c393ecbd Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Fri, 31 May 2024 01:36:46 +0200 Subject: [PATCH 25/77] Bump python version to 3.10 --- .gitlab-ci.yml | 2 +- INSTALL.md | 6 +++--- Utils/setup.sh | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index fe4d4446..55ef5901 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/INSTALL.md b/INSTALL.md index c887af92..5344a998 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 1c951824..6a93207d 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 -- GitLab From 58bbe6a95f069a0b4e2bf1c6ccfc33a40bfc80c7 Mon Sep 17 00:00:00 2001 From: Felix Blanke <s6feblan@uni-bonn.de> Date: Fri, 31 May 2024 14:18:53 +0000 Subject: [PATCH 26/77] Merge AK category slots --- AKModel/availability/models.py | 10 +- AKModel/models.py | 162 +++++++++++++++++++++++++++++---- AKModel/views/ak.py | 22 ++--- 3 files changed, 163 insertions(+), 31 deletions(-) diff --git a/AKModel/availability/models.py b/AKModel/availability/models.py index 80546543..35814ee0 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 & diff --git a/AKModel/models.py b/AKModel/models.py index 49159427..2d58a456 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -1,5 +1,6 @@ import itertools import json +from dataclasses import dataclass from datetime import datetime, timedelta from typing import Iterable @@ -14,6 +15,35 @@ 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. @@ -164,8 +194,13 @@ class Event(models.Model): ) def _generate_slots_from_block( - self, start: datetime, end: datetime, slot_duration: timedelta, slot_index: int = 0 - ) -> Iterable[list[int, "Availability"]]: + 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`, @@ -179,8 +214,7 @@ class Event(models.Model): :param slot_index: index of the first timeslot. Defaults to 0. :yield: Block of optimizer timeslots as the discretization result. - :ytype: list of tuples, each consisisting of the timeslot id - and its availability to indicate its start and duration. + :ytype: list of TimeslotBlock """ # local import to prevent cyclic import # pylint: disable=import-outside-toplevel @@ -189,6 +223,9 @@ class Event(models.Model): current_slot_start = start previous_slot_start: datetime | None = None + if constraints is None: + constraints = set() + current_block = [] room_availabilities = list({ @@ -213,7 +250,9 @@ class Event(models.Model): yield current_block current_block = [] - current_block.append((slot_index, slot)) + current_block.append( + OptimizerTimeslot(avail=slot, idx=slot_index, constraints=constraints) + ) previous_slot_start = current_slot_start slot_index += 1 @@ -224,44 +263,113 @@ class Event(models.Model): return slot_index - def uniform_time_slots(self, *, slots_in_an_hour=1.0) -> Iterable[list[int, "Availability"]]: + 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 tuples, each consisisting of the timeslot id - and its availability to indicate its start and duration. + :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=1.0) -> Iterable[list[int, "Availability"]]: - """Discretize the all default slots into a blocks of timeslots. + 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 tuples, each consisisting of the timeslot id - and its availability to indicate its start and duration. + :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"): - # NOTE: We do not differentiate between different primary categories + 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. @@ -275,9 +383,9 @@ class Event(models.Model): slots_in_an_hour = schedule["input"]["timeslots"]["info"]["duration"] timeslot_dict = { - slot_idx: slot - for block in self.default_time_slots(slots_in_an_hour=slots_in_an_hour) - for slot_idx, slot in block + 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"]: @@ -286,8 +394,8 @@ class Event(models.Model): scheduled_slot["timeslot_ids"] = list(map(int, scheduled_slot["timeslot_ids"])) - start_timeslot = timeslot_dict[min(scheduled_slot["timeslot_ids"])] - end_timeslot = timeslot_dict[max(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 @@ -390,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. @@ -821,6 +943,10 @@ class AKSlot(models.Model): 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}") diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index 818f15d9..76bfdb28 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -50,6 +50,7 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): 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) @@ -69,7 +70,6 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): and self._test_slot_contained(slot, availabilities) ) - def get_queryset(self): return super().get_queryset().order_by("ak__track") @@ -110,38 +110,38 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): if (values := AKSlot.objects.select_related().filter(ak__pk=ak_id, fixed=True)).exists() } - for block in self.event.default_time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR): + for block in self.event.merge_blocks(self.event.default_time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR)): current_block = [] - for slot_index, slot in block: + for timeslot in block: time_constraints = [] - - if self.event.reso_deadline is None or slot.end < self.event.reso_deadline: + 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(slot, availabilities) - or self._test_fixed_ak(ak_id, slot, ak_fixed) + 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(slot, availabilities) + 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(slot, availabilities) + if self._test_add_constraint(timeslot.avail, availabilities) ]) + time_constraints.extend(timeslot.constraints) current_block.append({ - "id": str(slot_index), + "id": str(timeslot.idx), "info": { - "start": slot.simplified, + "start": timeslot.avail.simplified, }, "fulfilled_time_constraints": time_constraints, }) -- GitLab From 326da97b7d4bbe7b896c08ace44d3cc1315d210b Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Fri, 31 May 2024 16:23:17 +0200 Subject: [PATCH 27/77] Update language files --- AKModel/locale/de_DE/LC_MESSAGES/django.po | 428 ++++++++++----------- 1 file changed, 214 insertions(+), 214 deletions(-) diff --git a/AKModel/locale/de_DE/LC_MESSAGES/django.po b/AKModel/locale/de_DE/LC_MESSAGES/django.po index 1026edf9..4cc752ae 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-05-27 02:31+0000\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,17 +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:437 AKModel/models.py:759 +#: 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:97 +#: AKModel/views/manage.py:75 AKModel/views/status.py:97 msgid "AKs" msgstr "AKs" @@ -59,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:184 +#: 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:199 +#: AKModel/admin.py:330 AKModel/views/ak.py:241 msgid "Reset AKs' interest counters" msgstr "Interessenszähler der AKs zurücksetzen" @@ -71,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" @@ -108,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:61 AKModel/models.py:235 -#: AKModel/models.py:328 AKModel/models.py:347 AKModel/models.py:373 -#: AKModel/models.py:427 AKModel/models.py:569 AKModel/models.py:608 -#: AKModel/models.py:698 AKModel/models.py:755 AKModel/models.py:946 +#: 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:236 -#: AKModel/models.py:329 AKModel/models.py:348 AKModel/models.py:374 -#: AKModel/models.py:428 AKModel/models.py:570 AKModel/models.py:609 -#: AKModel/models.py:699 AKModel/models.py:756 AKModel/models.py:947 +#: 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" @@ -130,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:573 -#: AKModel/models.py:598 AKModel/models.py:765 +#: AKModel/availability/models.py:61 AKModel/models.py:748 +#: AKModel/models.py:810 AKModel/models.py:1034 msgid "Room" msgstr "Raum" @@ -139,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:436 -#: AKModel/models.py:597 AKModel/models.py:693 +#: AKModel/availability/models.py:70 AKModel/models.py:611 +#: AKModel/models.py:809 AKModel/models.py:962 msgid "AK" msgstr "AK" @@ -148,8 +148,8 @@ msgstr "AK" msgid "AK whose availability this is" msgstr "Verfügbarkeiten" -#: AKModel/availability/models.py:79 AKModel/models.py:332 -#: AKModel/models.py:771 +#: AKModel/availability/models.py:79 AKModel/models.py:493 +#: AKModel/models.py:1040 msgid "AK Category" msgstr "AK-Kategorie" @@ -157,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" @@ -219,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:940 +#: AKModel/forms.py:189 AKModel/models.py:1209 msgid "Default Slots" msgstr "Standardslots" @@ -258,15 +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/forms.py:281 +#: AKModel/forms.py:282 msgid "JSON data" msgstr "JSON-Daten" -#: AKModel/forms.py:282 +#: 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:30 +#: AKModel/metaviews/admin.py:156 AKModel/models.py:60 msgid "Start" msgstr "Start" @@ -291,66 +291,66 @@ msgstr "Aktivieren?" msgid "Finish" msgstr "Abschluss" -#: AKModel/models.py:21 AKModel/models.py:320 AKModel/models.py:344 -#: AKModel/models.py:371 AKModel/models.py:389 AKModel/models.py:561 +#: 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:22 +#: AKModel/models.py:52 msgid "Name or iteration of the event" msgstr "Name oder Iteration des Events" -#: AKModel/models.py:23 +#: AKModel/models.py:53 msgid "Short Form" msgstr "Kurzer Name" -#: AKModel/models.py:24 +#: 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:26 +#: AKModel/models.py:56 msgid "Place" msgstr "Ort" -#: AKModel/models.py:27 +#: AKModel/models.py:57 msgid "City etc. the event takes place in" msgstr "Stadt o.ä. in der das Event stattfindet" -#: AKModel/models.py:29 +#: AKModel/models.py:59 msgid "Time Zone" msgstr "Zeitzone" -#: AKModel/models.py:29 +#: AKModel/models.py:59 msgid "Time Zone where this event takes place in" msgstr "Zeitzone in der das Event stattfindet" -#: AKModel/models.py:30 +#: AKModel/models.py:60 msgid "Time the event begins" msgstr "Zeit zu der das Event beginnt" -#: AKModel/models.py:31 +#: AKModel/models.py:61 msgid "End" msgstr "Ende" -#: AKModel/models.py:31 +#: AKModel/models.py:61 msgid "Time the event ends" msgstr "Zeit zu der das Event endet" -#: AKModel/models.py:32 +#: AKModel/models.py:62 msgid "Resolution Deadline" msgstr "Resolutionsdeadline" -#: AKModel/models.py:33 +#: 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:35 +#: AKModel/models.py:65 msgid "Interest Window Start" msgstr "Beginn Interessensbekundung" -#: AKModel/models.py:37 +#: AKModel/models.py:67 msgid "" "Opening time for expression of interest. When left blank, no interest " "indication will be possible." @@ -358,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:39 +#: AKModel/models.py:69 msgid "Interest Window End" msgstr "Ende Interessensbekundung" -#: AKModel/models.py:40 +#: AKModel/models.py:70 msgid "Closing time for expression of interest." msgstr "Öffnungszeitpunkt für die Angabe von Interesse an AKs." -#: AKModel/models.py:42 +#: AKModel/models.py:72 msgid "Public event" msgstr "Öffentliches Event" -#: AKModel/models.py:43 +#: AKModel/models.py:73 msgid "Show this event on overview page." msgstr "Zeige dieses Event auf der Übersichtseite an" -#: AKModel/models.py:45 +#: AKModel/models.py:75 msgid "Active State" msgstr "Aktiver Status" -#: AKModel/models.py:45 +#: AKModel/models.py:75 msgid "Marks currently active events" msgstr "Markiert aktuell aktive Events" -#: AKModel/models.py:46 +#: AKModel/models.py:76 msgid "Plan Hidden" msgstr "Plan verborgen" -#: AKModel/models.py:46 +#: 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:48 +#: AKModel/models.py:78 msgid "Plan published at" msgstr "Plan veröffentlicht am/um" -#: AKModel/models.py:49 +#: AKModel/models.py:79 msgid "Timestamp at which the plan was published" msgstr "Zeitpunkt, zu dem der Plan veröffentlicht wurde" -#: AKModel/models.py:51 +#: AKModel/models.py:81 msgid "Base URL" msgstr "URL-Prefix" -#: AKModel/models.py:51 +#: AKModel/models.py:81 msgid "Prefix for wiki link construction" msgstr "Prefix für die automatische Generierung von Wiki-Links" -#: AKModel/models.py:52 +#: AKModel/models.py:82 msgid "Wiki Export Template Name" msgstr "Wiki-Export Templatename" -#: AKModel/models.py:53 +#: AKModel/models.py:83 msgid "Default Slot Length" msgstr "Standardslotlänge" -#: AKModel/models.py:54 +#: 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:56 +#: AKModel/models.py:86 msgid "Contact email address" msgstr "E-Mail Kontaktadresse" -#: AKModel/models.py:57 +#: AKModel/models.py:87 msgid "" "An email address that is displayed on every page and can be used for all " "kinds of questions" @@ -430,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:62 +#: AKModel/models.py:92 msgid "Events" msgstr "Events" -#: AKModel/models.py:230 +#: AKModel/models.py:407 msgid "Nickname" msgstr "Spitzname" -#: AKModel/models.py:230 +#: AKModel/models.py:407 msgid "Name to identify an AK owner by" msgstr "Name, durch den eine AK-Leitung identifiziert wird" -#: AKModel/models.py:231 +#: AKModel/models.py:408 msgid "Slug" msgstr "Slug" -#: AKModel/models.py:231 +#: AKModel/models.py:408 msgid "Slug for URL generation" msgstr "Slug für URL-Generierung" -#: AKModel/models.py:232 +#: AKModel/models.py:409 msgid "Institution" msgstr "Instutution" -#: AKModel/models.py:232 +#: AKModel/models.py:409 msgid "Uni etc." msgstr "Universität o.ä." -#: AKModel/models.py:233 AKModel/models.py:398 +#: AKModel/models.py:410 AKModel/models.py:573 msgid "Web Link" msgstr "Internet Link" -#: AKModel/models.py:233 +#: AKModel/models.py:410 msgid "Link to Homepage" msgstr "Link zu Homepage oder Webseite" -#: AKModel/models.py:239 AKModel/models.py:764 +#: AKModel/models.py:416 AKModel/models.py:1033 msgid "AK Owner" msgstr "AK-Leitung" -#: AKModel/models.py:240 +#: AKModel/models.py:417 msgid "AK Owners" msgstr "AK-Leitungen" -#: AKModel/models.py:320 +#: AKModel/models.py:481 msgid "Name of the AK Category" msgstr "Name der AK-Kategorie" -#: AKModel/models.py:321 AKModel/models.py:345 +#: AKModel/models.py:482 AKModel/models.py:520 msgid "Color" msgstr "Farbe" -#: AKModel/models.py:321 AKModel/models.py:345 +#: AKModel/models.py:482 AKModel/models.py:520 msgid "Color for displaying" msgstr "Farbe für die Anzeige" -#: AKModel/models.py:322 AKModel/models.py:392 +#: AKModel/models.py:483 AKModel/models.py:567 msgid "Description" msgstr "Beschreibung" -#: AKModel/models.py:323 +#: AKModel/models.py:484 msgid "Short description of this AK Category" msgstr "Beschreibung der AK-Kategorie" -#: AKModel/models.py:324 +#: AKModel/models.py:485 msgid "Present by default" msgstr "Defaultmäßig präsentieren" -#: AKModel/models.py:325 +#: AKModel/models.py:486 msgid "" "Present AKs of this category by default if AK owner did not specify whether " "this AK should be presented?" @@ -506,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:333 +#: AKModel/models.py:494 msgid "AK Categories" msgstr "AK-Kategorien" -#: AKModel/models.py:344 +#: AKModel/models.py:519 msgid "Name of the AK Track" msgstr "Name des AK-Tracks" -#: AKModel/models.py:351 +#: AKModel/models.py:526 msgid "AK Track" msgstr "AK-Track" -#: AKModel/models.py:352 +#: AKModel/models.py:527 msgid "AK Tracks" msgstr "AK-Tracks" -#: AKModel/models.py:371 +#: AKModel/models.py:546 msgid "Name of the Requirement" msgstr "Name der Anforderung" -#: AKModel/models.py:377 AKModel/models.py:768 +#: AKModel/models.py:552 AKModel/models.py:1037 msgid "AK Requirement" msgstr "AK-Anforderung" -#: AKModel/models.py:378 +#: AKModel/models.py:553 msgid "AK Requirements" msgstr "AK-Anforderungen" -#: AKModel/models.py:389 +#: AKModel/models.py:564 msgid "Name of the AK" msgstr "Name des AKs" -#: AKModel/models.py:390 +#: AKModel/models.py:565 msgid "Short Name" msgstr "Kurzer Name" -#: AKModel/models.py:391 +#: AKModel/models.py:566 msgid "Name displayed in the schedule" msgstr "Name zur Anzeige im AK-Plan" -#: AKModel/models.py:392 +#: AKModel/models.py:567 msgid "Description of the AK" msgstr "Beschreibung des AKs" -#: AKModel/models.py:394 +#: AKModel/models.py:569 msgid "Owners" msgstr "Leitungen" -#: AKModel/models.py:395 +#: AKModel/models.py:570 msgid "Those organizing the AK" msgstr "Menschen, die den AK organisieren und halten" -#: AKModel/models.py:398 +#: AKModel/models.py:573 msgid "Link to wiki page" msgstr "Link zur Wiki Seite" -#: AKModel/models.py:399 +#: AKModel/models.py:574 msgid "Protocol Link" msgstr "Protokolllink" -#: AKModel/models.py:399 +#: AKModel/models.py:574 msgid "Link to protocol" msgstr "Link zum Protokoll" -#: AKModel/models.py:401 +#: AKModel/models.py:576 msgid "Category" msgstr "Kategorie" -#: AKModel/models.py:402 +#: AKModel/models.py:577 msgid "Category of the AK" msgstr "Kategorie des AKs" -#: AKModel/models.py:403 +#: AKModel/models.py:578 msgid "Track" msgstr "Track" -#: AKModel/models.py:404 +#: AKModel/models.py:579 msgid "Track the AK belongs to" msgstr "Track zu dem der AK gehört" -#: AKModel/models.py:406 +#: AKModel/models.py:581 msgid "Resolution Intention" msgstr "Resolutionsabsicht" -#: AKModel/models.py:407 +#: AKModel/models.py:582 msgid "Intends to submit a resolution" msgstr "Beabsichtigt eine Resolution einzureichen" -#: AKModel/models.py:408 +#: AKModel/models.py:583 msgid "Present this AK" msgstr "AK präsentieren" -#: AKModel/models.py:409 +#: AKModel/models.py:584 msgid "Present results of this AK" msgstr "Die Ergebnisse dieses AKs vorstellen" -#: AKModel/models.py:411 AKModel/views/status.py:170 +#: AKModel/models.py:586 AKModel/views/status.py:170 msgid "Requirements" msgstr "Anforderungen" -#: AKModel/models.py:412 +#: AKModel/models.py:587 msgid "AK's Requirements" msgstr "Anforderungen des AKs" -#: AKModel/models.py:414 +#: AKModel/models.py:589 msgid "Conflicting AKs" msgstr "AK-Konflikte" -#: AKModel/models.py:415 +#: 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:416 +#: AKModel/models.py:591 msgid "Prerequisite AKs" msgstr "Vorausgesetzte AKs" -#: AKModel/models.py:417 +#: 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:419 +#: AKModel/models.py:594 msgid "Organizational Notes" msgstr "Notizen zur Organisation" -#: AKModel/models.py:420 +#: 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/" @@ -641,291 +641,291 @@ msgstr "" "Anmerkungen bitte den Button für Direktnachrichten verwenden (nach dem " "Anlegen/Bearbeiten)." -#: AKModel/models.py:423 +#: AKModel/models.py:598 msgid "Interest" msgstr "Interesse" -#: AKModel/models.py:423 +#: AKModel/models.py:598 msgid "Expected number of people" msgstr "Erwartete Personenzahl" -#: AKModel/models.py:424 +#: AKModel/models.py:599 msgid "Interest Counter" msgstr "Interessenszähler" -#: AKModel/models.py:425 +#: AKModel/models.py:600 msgid "People who have indicated interest online" msgstr "Anzahl Personen, die online Interesse bekundet haben" -#: AKModel/models.py:430 +#: AKModel/models.py:605 msgid "Export?" msgstr "Export?" -#: AKModel/models.py:431 +#: AKModel/models.py:606 msgid "Include AK in wiki export?" msgstr "AK bei Wiki-Export berücksichtigen?" -#: AKModel/models.py:561 +#: AKModel/models.py:736 msgid "Name or number of the room" msgstr "Name oder Nummer des Raums" -#: AKModel/models.py:562 +#: AKModel/models.py:737 msgid "Location" msgstr "Ort" -#: AKModel/models.py:563 +#: AKModel/models.py:738 msgid "Name or number of the location" msgstr "Name oder Nummer des Ortes" -#: AKModel/models.py:564 +#: AKModel/models.py:739 msgid "Capacity" msgstr "Kapazität" -#: AKModel/models.py:565 +#: AKModel/models.py:740 msgid "Maximum number of people (-1 for unlimited)." msgstr "Maximale Personenzahl (-1 wenn unbeschränkt)." -#: AKModel/models.py:566 +#: AKModel/models.py:741 msgid "Properties" msgstr "Eigenschaften" -#: AKModel/models.py:567 +#: AKModel/models.py:742 msgid "AK requirements fulfilled by the room" msgstr "AK-Anforderungen, die dieser Raum erfüllt" -#: AKModel/models.py:574 AKModel/views/status.py:59 +#: AKModel/models.py:749 AKModel/views/status.py:59 msgid "Rooms" msgstr "Räume" -#: AKModel/models.py:597 +#: AKModel/models.py:809 msgid "AK being mapped" msgstr "AK, der zugeordnet wird" -#: AKModel/models.py:599 +#: AKModel/models.py:811 msgid "Room the AK will take place in" msgstr "Raum in dem der AK stattfindet" -#: AKModel/models.py:600 AKModel/models.py:943 +#: AKModel/models.py:812 AKModel/models.py:1212 msgid "Slot Begin" msgstr "Beginn des Slots" -#: AKModel/models.py:600 AKModel/models.py:943 +#: 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:602 +#: AKModel/models.py:814 msgid "Duration" msgstr "Dauer" -#: AKModel/models.py:603 +#: AKModel/models.py:815 msgid "Length in hours" msgstr "Länge in Stunden" -#: AKModel/models.py:605 +#: AKModel/models.py:817 msgid "Scheduling fixed" msgstr "Planung fix" -#: AKModel/models.py:606 +#: 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:611 +#: AKModel/models.py:823 msgid "Last update" msgstr "Letzte Aktualisierung" -#: AKModel/models.py:614 +#: AKModel/models.py:826 msgid "AK Slot" msgstr "AK-Slot" -#: AKModel/models.py:615 AKModel/models.py:761 +#: AKModel/models.py:827 AKModel/models.py:1030 msgid "AK Slots" msgstr "AK-Slot" -#: AKModel/models.py:637 AKModel/models.py:646 +#: AKModel/models.py:849 AKModel/models.py:858 msgid "Not scheduled yet" msgstr "Noch nicht geplant" -#: AKModel/models.py:694 +#: AKModel/models.py:963 msgid "AK this message belongs to" msgstr "AK zu dem die Nachricht gehört" -#: AKModel/models.py:695 +#: AKModel/models.py:964 msgid "Message text" msgstr "Nachrichtentext" -#: AKModel/models.py:696 +#: 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:700 +#: AKModel/models.py:969 msgid "Resolved" msgstr "Erledigt" -#: AKModel/models.py:701 +#: 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)" -#: AKModel/models.py:704 +#: AKModel/models.py:973 msgid "AK Orga Message" msgstr "AK-Organachricht" -#: AKModel/models.py:705 +#: AKModel/models.py:974 msgid "AK Orga Messages" msgstr "AK-Organachrichten" -#: AKModel/models.py:722 +#: AKModel/models.py:991 msgid "Constraint Violation" msgstr "Constraintverletzung" -#: AKModel/models.py:723 +#: AKModel/models.py:992 msgid "Constraint Violations" msgstr "Constraintverletzungen" -#: AKModel/models.py:730 +#: AKModel/models.py:999 msgid "Owner has two parallel slots" msgstr "Leitung hat zwei Slots parallel" -#: AKModel/models.py:731 +#: 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:732 +#: 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:733 +#: 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:734 +#: 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:735 +#: 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:737 +#: 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:738 +#: 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:739 +#: 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:740 +#: 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:741 +#: 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:747 +#: AKModel/models.py:1016 msgid "Warning" msgstr "Warnung" -#: AKModel/models.py:748 +#: AKModel/models.py:1017 msgid "Violation" msgstr "Verletzung" -#: AKModel/models.py:750 +#: AKModel/models.py:1019 msgid "Type" msgstr "Art" -#: AKModel/models.py:751 +#: 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:752 +#: AKModel/models.py:1021 msgid "Level" msgstr "Level" -#: AKModel/models.py:753 +#: AKModel/models.py:1022 msgid "Severity level of the violation" msgstr "Schweregrad der Verletzung" -#: AKModel/models.py:760 +#: AKModel/models.py:1029 msgid "AK(s) belonging to this constraint" msgstr "AK(s), die zu diesem Constraint gehören" -#: AKModel/models.py:762 +#: 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:764 +#: AKModel/models.py:1033 msgid "AK Owner belonging to this constraint" msgstr "AK Leitung(en), die zu diesem Constraint gehören" -#: AKModel/models.py:766 +#: AKModel/models.py:1035 msgid "Room belonging to this constraint" msgstr "Raum, der zu diesem Constraint gehört" -#: AKModel/models.py:769 +#: AKModel/models.py:1038 msgid "AK Requirement belonging to this constraint" msgstr "AK Anforderung, die zu diesem Constraint gehört" -#: AKModel/models.py:771 +#: AKModel/models.py:1040 msgid "AK Category belonging to this constraint" msgstr "AK Kategorie, di zu diesem Constraint gehört" -#: AKModel/models.py:773 +#: AKModel/models.py:1042 msgid "Comment" msgstr "Kommentar" -#: AKModel/models.py:773 +#: AKModel/models.py:1042 msgid "Comment or further details for this violation" msgstr "Kommentar oder weitere Details zu dieser Vereletzung" -#: AKModel/models.py:776 +#: AKModel/models.py:1045 msgid "Timestamp" msgstr "Timestamp" -#: AKModel/models.py:776 +#: AKModel/models.py:1045 msgid "Time of creation" msgstr "Zeitpunkt der ERstellung" -#: AKModel/models.py:777 +#: AKModel/models.py:1046 msgid "Manually Resolved" msgstr "Manuell behoben" -#: AKModel/models.py:778 +#: AKModel/models.py:1047 msgid "Mark this violation manually as resolved" msgstr "Markiere diese Verletzung manuell als behoben" -#: AKModel/models.py:805 +#: AKModel/models.py:1074 #: AKModel/templates/admin/AKModel/requirements_overview.html:27 msgid "Details" msgstr "Details" -#: AKModel/models.py:939 +#: AKModel/models.py:1208 msgid "Default Slot" msgstr "Standardslot" -#: AKModel/models.py:944 +#: AKModel/models.py:1213 msgid "Slot End" msgstr "Ende des Slots" -#: AKModel/models.py:944 +#: AKModel/models.py:1213 msgid "Time and date the slot ends" msgstr "Zeit und Datum zu der der Slot endet" -#: AKModel/models.py:949 +#: AKModel/models.py:1218 msgid "Primary categories" msgstr "Primäre Kategorien" -#: AKModel/models.py:950 +#: AKModel/models.py:1219 msgid "Categories that should be assigned to this slot primarily" msgstr "Kategorieren, die diesem Slot primär zugewiesen werden sollen" @@ -1123,47 +1123,47 @@ msgstr "Login" msgid "Register" msgstr "Registrieren" -#: AKModel/views/ak.py:20 +#: AKModel/views/ak.py:21 msgid "Requirements for Event" msgstr "Anforderungen für das Event" -#: AKModel/views/ak.py:37 +#: AKModel/views/ak.py:38 msgid "AK CSV Export" msgstr "AK-CSV-Export" -#: AKModel/views/ak.py:50 +#: AKModel/views/ak.py:51 msgid "AK JSON Export" msgstr "AK-JSON-Export" -#: AKModel/views/ak.py:133 +#: AKModel/views/ak.py:175 msgid "AK Wiki Export" msgstr "AK-Wiki-Export" -#: AKModel/views/ak.py:144 AKModel/views/manage.py:53 +#: AKModel/views/ak.py:186 AKModel/views/manage.py:55 msgid "Wishes" msgstr "Wünsche" -#: AKModel/views/ak.py:156 +#: AKModel/views/ak.py:198 msgid "Delete AK Orga Messages" msgstr "AK-Organachrichten löschen" -#: AKModel/views/ak.py:174 +#: AKModel/views/ak.py:216 msgid "AK Orga Messages successfully deleted" msgstr "AK-Organachrichten erfolgreich gelöscht" -#: AKModel/views/ak.py:186 +#: 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:187 +#: AKModel/views/ak.py:229 msgid "Reset of interest in AKs successful." msgstr "Interesse an AKs erfolgreich zurückgesetzt." -#: AKModel/views/ak.py:201 +#: 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:202 +#: AKModel/views/ak.py:244 msgid "AKs' interest counters set back to 0." msgstr "Interessenszähler der AKs zurückgesetzt" @@ -1177,91 +1177,91 @@ 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:153 +#: 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:129 +#: 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:252 +#: AKModel/views/manage.py:257 msgid "AK JSON Import" msgstr "AK-JSON-Import" -- GitLab From 25a3b3f9c322b1862ef444e7b753f68f143284ed Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Thu, 26 Dec 2024 18:51:18 +0100 Subject: [PATCH 28/77] Rename json import to make clear that a schedule is imported --- AKModel/forms.py | 2 +- AKModel/locale/de_DE/LC_MESSAGES/django.po | 4 ++-- AKModel/models.py | 2 +- AKModel/urls.py | 6 +++--- AKModel/views/manage.py | 8 ++++---- AKModel/views/status.py | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/AKModel/forms.py b/AKModel/forms.py index 74ca1b68..bf77085c 100644 --- a/AKModel/forms.py +++ b/AKModel/forms.py @@ -274,7 +274,7 @@ class RoomFormWithAvailabilities(AvailabilitiesFormMixin, RoomForm): self.fields["properties"].queryset = AKRequirement.objects.filter(event=self.instance.event) -class JSONImportForm(AdminIntermediateForm): +class JSONScheduleImportForm(AdminIntermediateForm): """Form to import an AK schedule from a json file.""" json_data = forms.CharField( required=True, diff --git a/AKModel/locale/de_DE/LC_MESSAGES/django.po b/AKModel/locale/de_DE/LC_MESSAGES/django.po index 4cc752ae..4fcbd8a8 100644 --- a/AKModel/locale/de_DE/LC_MESSAGES/django.po +++ b/AKModel/locale/de_DE/LC_MESSAGES/django.po @@ -1262,8 +1262,8 @@ 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" +msgid "AK Schedule JSON Import" +msgstr "AK-Plan JSON-Import" #: AKModel/views/room.py:37 #, python-format diff --git a/AKModel/models.py b/AKModel/models.py index 2d58a456..fdb03256 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -631,7 +631,7 @@ class AK(models.Model): availabilities = ', \n'.join(f'{a.simplified}' for a in Availability.objects.select_related('event') .filter(ak=self)) detail_string = f"""{self.name}{" (R)" if self.reso else ""}: - + {self.owners_list} {_('Interest')}: {self.interest}""" diff --git a/AKModel/urls.py b/AKModel/urls.py index 9871b411..9c103405 100644 --- a/AKModel/urls.py +++ b/AKModel/urls.py @@ -5,7 +5,7 @@ from rest_framework.routers import DefaultRouter import AKModel.views.api from AKModel.views.manage import ExportSlidesView, PlanPublishView, PlanUnpublishView, DefaultSlotEditorView, \ - AKsByUserView, AKJSONImportView + AKsByUserView, AKScheduleJSONImportView from AKModel.views.ak import AKRequirementOverview, AKCSVExportView, AKJSONExportView, AKWikiExportView, \ AKMessageDeleteView from AKModel.views.event_wizard import NewEventWizardStartView, NewEventWizardPrepareImportView, \ @@ -99,8 +99,8 @@ def get_admin_urls_event(admin_site): 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:event_slug>/ak-schedule-json-import/', admin_site.admin_view(AKScheduleJSONImportView.as_view()), + name="ak_schedule_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/manage.py b/AKModel/views/manage.py index ec5076fb..1bad9534 100644 --- a/AKModel/views/manage.py +++ b/AKModel/views/manage.py @@ -14,7 +14,7 @@ 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, JSONImportForm +from AKModel.forms import SlideExportForm, DefaultSlotEditorForm, JSONScheduleImportForm from AKModel.metaviews.admin import EventSlugMixin, IntermediateAdminView, IntermediateAdminActionView, AdminViewMixin from AKModel.models import ConstraintViolation, Event, DefaultSlot, AKOwner @@ -249,12 +249,12 @@ class AKsByUserView(AdminViewMixin, EventSlugMixin, DetailView): template_name = "admin/AKModel/aks_by_user.html" -class AKJSONImportView(EventSlugMixin, IntermediateAdminView): +class AKScheduleJSONImportView(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") + form_class = JSONScheduleImportForm + title = _("AK Schedule JSON Import") def form_valid(self, form): self.event.schedule_from_json(form.data["json_data"]) diff --git a/AKModel/views/status.py b/AKModel/views/status.py index 0c12b303..f7baa0da 100644 --- a/AKModel/views/status.py +++ b/AKModel/views/status.py @@ -135,7 +135,7 @@ class EventAKsWidget(TemplateStatusWidget): }, { "text": _("Import AK schedule from JSON"), - "url": reverse_lazy("admin:ak_json_import", kwargs={"event_slug": context["event"].slug}), + "url": reverse_lazy("admin:ak_schedule_json_import", kwargs={"event_slug": context["event"].slug}), }, { "text": _("Export AKs as CSV"), -- GitLab From 1b1b56fda4864489dba7e8d6991154e26b4c042b Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Thu, 26 Dec 2024 19:02:38 +0100 Subject: [PATCH 29/77] Check all blocks of union if event is covered --- AKModel/availability/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AKModel/availability/models.py b/AKModel/availability/models.py index 35814ee0..27a6c228 100644 --- a/AKModel/availability/models.py +++ b/AKModel/availability/models.py @@ -293,7 +293,7 @@ class Availability(models.Model): # 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) + return any(avail.contains(full_event) for avail in avail_union) class Meta: verbose_name = _('Availability') -- GitLab From cf082b61b7e1a52cd118ad2abe5173b5d7b9159f Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Thu, 26 Dec 2024 19:28:42 +0100 Subject: [PATCH 30/77] Improve docstring and type hints for optimizer slots --- AKModel/models.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/AKModel/models.py b/AKModel/models.py index fdb03256..08e3d371 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -2,7 +2,7 @@ import itertools import json from dataclasses import dataclass from datetime import datetime, timedelta -from typing import Iterable +from typing import Iterable, Generator from django.db import models from django.apps import apps @@ -17,10 +17,15 @@ from timezone_field import TimeZoneField @dataclass class OptimizerTimeslot: - """Class describing a timeslot. Used to interface with an optimizer.""" + """Class describing a discrete timeslot. Used to interface with an optimizer.""" + """The availability object corresponding to this timeslot.""" avail: "Availability" + + """The unique index of this optimizer timeslot.""" idx: int + + """The set of time constraints fulfilled by this object.""" constraints: set[str] def merge(self, other: "OptimizerTimeslot") -> "OptimizerTimeslot": @@ -33,7 +38,6 @@ class OptimizerTimeslot: """ 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 ) @@ -200,10 +204,10 @@ class Event(models.Model): slot_duration: timedelta, slot_index: int = 0, constraints: set[str] | None = None, - ) -> Iterable[TimeslotBlock]: + ) -> Generator[TimeslotBlock, None, int]: """Discretize a time range into timeslots. - Uses a uniform discretization into blocks of length `slot_duration`, + Uses a uniform discretization into discrete slots 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. @@ -214,7 +218,11 @@ class Event(models.Model): :param slot_index: index of the first timeslot. Defaults to 0. :yield: Block of optimizer timeslots as the discretization result. - :ytype: list of TimeslotBlock + :ytype: list of OptimizerTimeslot + + :return: The first slot index after the yielded blocks, i.e. + `slot_index` + total # generated timeslots + :rtype: int """ # local import to prevent cyclic import # pylint: disable=import-outside-toplevel @@ -264,12 +272,15 @@ class Event(models.Model): 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. + """Uniformly discretize the entire event into blocks of timeslots. + + Discretizes entire event uniformly. May not necessarily result in a single block + as slots with no room availability are dropped. :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 + :ytype: list of OptimizerTimeslot """ all_category_constraints = AKCategory.create_category_constraints( AKCategory.objects.filter(event=self).all() -- GitLab From fd6adb0e83d9fece3feabbded3af9677700393f0 Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Thu, 26 Dec 2024 19:29:59 +0100 Subject: [PATCH 31/77] Use round instead of int to calc duration --- AKModel/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AKModel/models.py b/AKModel/models.py index 08e3d371..494e5911 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -936,7 +936,7 @@ class AKSlot(models.Model): # self.slots_in_an_hour is set in AKJSONExportView data = { "id": str(self.pk), - "duration": int(self.duration * self.slots_in_an_hour), + "duration": round(self.duration * self.slots_in_an_hour), "properties": {}, "room_constraints": [constraint.name for constraint in self.ak.requirements.all()], -- GitLab From 61922712b361a51fe2979478b774ff74226136c6 Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Thu, 26 Dec 2024 19:35:14 +0100 Subject: [PATCH 32/77] Detach merge_blocks from Event object --- AKModel/models.py | 127 ++++++++++++++++++++++---------------------- AKModel/views/ak.py | 4 +- 2 files changed, 66 insertions(+), 65 deletions(-) diff --git a/AKModel/models.py b/AKModel/models.py index 494e5911..3b6eb600 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -48,6 +48,69 @@ class OptimizerTimeslot: TimeslotBlock = list[OptimizerTimeslot] +def merge_blocks( + 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 + + class Event(models.Model): """ An event supplies the frame for all Aks. @@ -319,68 +382,6 @@ class Event(models.Model): 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. @@ -395,7 +396,7 @@ class Event(models.Model): timeslot_dict = { timeslot.idx: timeslot - for block in self.merge_blocks(self.default_time_slots(slots_in_an_hour=slots_in_an_hour)) + for block in merge_blocks(self.default_time_slots(slots_in_an_hour=slots_in_an_hour)) for timeslot in block } diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index 76bfdb28..0ee77537 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -9,7 +9,7 @@ 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, Room, AKOwner +from AKModel.models import AKRequirement, AKSlot, Event, AKOrgaMessage, AK, Room, AKOwner, merge_blocks class AKRequirementOverview(AdminViewMixin, FilterByEventSlugMixin, ListView): @@ -110,7 +110,7 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): 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)): + for block in merge_blocks(self.event.default_time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR)): current_block = [] for timeslot in block: -- GitLab From 8d049baab9c3dd23c9627b4feee1e60be71904de Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Fri, 27 Dec 2024 14:58:46 +0100 Subject: [PATCH 33/77] Make tests more descriptive --- AKModel/views/ak.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index 0ee77537..44fbc096 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -52,12 +52,15 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): def _test_slot_contained(self, slot: Availability, availabilities: List[Availability]) -> bool: + """Test if slot is contained in any member of availabilities.""" return any(availability.contains(slot) for availability in availabilities) - def _test_event_covered(self, availabilities: List[Availability]) -> bool: + def _test_event_not_covered(self, availabilities: List[Availability]) -> bool: + """Test if event is not covered by availabilities.""" return not Availability.is_event_covered(self.event, availabilities) - def _test_fixed_ak(self, ak_id, slot: Availability, ak_fixed: dict) -> bool: + def _test_ak_fixed_in_slot(self, ak_id, slot: Availability, ak_fixed: dict) -> bool: + """Test if AK defined by `ak_id` is fixed to happen during slot.""" if not ak_id in ak_fixed: return False @@ -65,8 +68,9 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): return fixed_slot.overlaps(slot, strict=True) def _test_add_constraint(self, slot: Availability, availabilities: List[Availability]) -> bool: + """Test if object is not available for whole event and may happen during slot.""" return ( - self._test_event_covered(availabilities) + self._test_event_not_covered(availabilities) and self._test_slot_contained(slot, availabilities) ) @@ -123,7 +127,7 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): 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) + or self._test_ak_fixed_in_slot(ak_id, timeslot.avail, ak_fixed) ) ]) time_constraints.extend([ -- GitLab From c73020b00c9138e9bca6ede2809829fef1a01d32 Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Fri, 27 Dec 2024 14:59:11 +0100 Subject: [PATCH 34/77] Use uniform discretization if no default slots exist --- AKModel/views/ak.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index 44fbc096..93d790f8 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -114,7 +114,13 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): if (values := AKSlot.objects.select_related().filter(ak__pk=ak_id, fixed=True)).exists() } - for block in merge_blocks(self.event.default_time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR)): + if DefaultSlot.objects.filter(event=self).exists(): + # discretize default slots if they exists + blocks = merge_blocks(self.event.default_time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR)) + else: + blocks = self.event.uniform_time_slots(slos_in_an_hour=SLOTS_IN_AN_HOUR) + + for block in blocks: current_block = [] for timeslot in block: -- GitLab From 1661b3594255e1ccfbd919c3540b2daf762c9202 Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Fri, 27 Dec 2024 15:02:33 +0100 Subject: [PATCH 35/77] Add more comments --- AKModel/views/ak.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index 93d790f8..74c54b9e 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -125,9 +125,12 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): for timeslot in block: time_constraints = [] + # if reso_deadline is set and timeslot ends before it, + # add fulfilled time constraint 'resolution' if self.event.reso_deadline is None or timeslot.avail.end < self.event.reso_deadline: time_constraints.append("resolution") + # add fulfilled time constraints for all AKs that cannot happen during full event time_constraints.extend([ f"availability-ak-{ak_id}" for ak_id, availabilities in ak_availabilities.items() @@ -136,11 +139,13 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): or self._test_ak_fixed_in_slot(ak_id, timeslot.avail, ak_fixed) ) ]) + # add fulfilled time constraints for all persons that are not available for full event time_constraints.extend([ f"availability-person-{person_id}" for person_id, availabilities in person_availabilities.items() if self._test_add_constraint(timeslot.avail, availabilities) ]) + # add fulfilled time constraints for all rooms that are not available for full event time_constraints.extend([ f"availability-room-{room_id}" for room_id, availabilities in room_availabilities.items() -- GitLab From 53dfe886b832a20b06e994db3db2883631d5f44c Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Sat, 28 Dec 2024 01:32:39 +0100 Subject: [PATCH 36/77] Fix docstr placement --- AKModel/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/AKModel/models.py b/AKModel/models.py index 3b6eb600..d30912d3 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -19,14 +19,14 @@ from timezone_field import TimeZoneField class OptimizerTimeslot: """Class describing a discrete timeslot. Used to interface with an optimizer.""" - """The availability object corresponding to this timeslot.""" avail: "Availability" + """The availability object corresponding to this timeslot.""" - """The unique index of this optimizer timeslot.""" idx: int + """The unique index of this optimizer timeslot.""" - """The set of time constraints fulfilled by this object.""" constraints: set[str] + """The set of time constraints fulfilled by this object.""" def merge(self, other: "OptimizerTimeslot") -> "OptimizerTimeslot": """Merge with other OptimizerTimeslot. -- GitLab From ec5c79e7b6a10908c337e18dc69e9d0dd78f8ef2 Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Sat, 28 Dec 2024 01:32:54 +0100 Subject: [PATCH 37/77] Fix import --- AKModel/views/ak.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index 74c54b9e..230ab247 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -9,7 +9,7 @@ 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, Room, AKOwner, merge_blocks +from AKModel.models import AKRequirement, AKSlot, DefaultSlot, Event, AKOrgaMessage, AK, Room, AKOwner, merge_blocks class AKRequirementOverview(AdminViewMixin, FilterByEventSlugMixin, ListView): -- GitLab From 05dbef0b4fe43fc163a834788ed0aeb2e4fc065c Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Sat, 28 Dec 2024 01:41:59 +0100 Subject: [PATCH 38/77] Add json import/export view to test for only internal access --- AKModel/tests.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AKModel/tests.py b/AKModel/tests.py index fb24dc08..6730315d 100644 --- a/AKModel/tests.py +++ b/AKModel/tests.py @@ -214,7 +214,9 @@ class ModelViewTests(BasicViewTests, TestCase): ('admin:event_status', {'event_slug': 'kif42'}), ('admin:event_requirement_overview', {'event_slug': 'kif42'}), ('admin:ak_csv_export', {'event_slug': 'kif42'}), + ('admin:ak_json_export', {'event_slug': 'kif42'}), ('admin:ak_wiki_export', {'slug': 'kif42'}), + ('admin:ak_schedule_json_import', {'event_slug': 'kif42'}), ('admin:ak_delete_orga_messages', {'event_slug': 'kif42'}), ('admin:ak_slide_export', {'event_slug': 'kif42'}), ('admin:default-slots-editor', {'event_slug': 'kif42'}), -- GitLab From 4505f33b8bc8c45d8f5034d1f9bd4be471ebb43f Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Sat, 28 Dec 2024 01:47:13 +0100 Subject: [PATCH 39/77] Fix event filter --- AKModel/views/ak.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index 230ab247..ecb766ac 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -114,7 +114,7 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): if (values := AKSlot.objects.select_related().filter(ak__pk=ak_id, fixed=True)).exists() } - if DefaultSlot.objects.filter(event=self).exists(): + if DefaultSlot.objects.filter(event=self.event).exists(): # discretize default slots if they exists blocks = merge_blocks(self.event.default_time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR)) else: -- GitLab From 1ae6e91655c8343dee69269adf55c09cbd9fb60c Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Sat, 28 Dec 2024 01:50:21 +0100 Subject: [PATCH 40/77] Fix typo --- AKModel/views/ak.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index ecb766ac..90599a1b 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -118,7 +118,7 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): # discretize default slots if they exists blocks = merge_blocks(self.event.default_time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR)) else: - blocks = self.event.uniform_time_slots(slos_in_an_hour=SLOTS_IN_AN_HOUR) + blocks = self.event.uniform_time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR) for block in blocks: current_block = [] -- GitLab From dafa0810a062eefe19482c1965b412cddb32ddfd Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Tue, 21 Jan 2025 16:16:39 +0100 Subject: [PATCH 41/77] Change rounding strategy to ceil --- AKModel/models.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/AKModel/models.py b/AKModel/models.py index d30912d3..636f7a9f 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -1,5 +1,6 @@ import itertools import json +import math from dataclasses import dataclass from datetime import datetime, timedelta from typing import Iterable, Generator @@ -934,10 +935,12 @@ class AKSlot(models.Model): return [] return [f"availability-person-{owner.pk}"] + ceil_offet_eps = 1e-4 + # self.slots_in_an_hour is set in AKJSONExportView data = { "id": str(self.pk), - "duration": round(self.duration * self.slots_in_an_hour), + "duration": math.ceil(self.duration * self.slots_in_an_hour - ceil_offet_eps), "properties": {}, "room_constraints": [constraint.name for constraint in self.ak.requirements.all()], -- GitLab From 3f8f7ad79bed1d3265a90a0c7b0efbfb61a1c30d Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Tue, 21 Jan 2025 16:56:41 +0100 Subject: [PATCH 42/77] change offset from float to decimal --- AKModel/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AKModel/models.py b/AKModel/models.py index 636f7a9f..0e995932 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -3,6 +3,7 @@ import json import math from dataclasses import dataclass from datetime import datetime, timedelta +from decimal import Decimal from typing import Iterable, Generator from django.db import models @@ -935,7 +936,7 @@ class AKSlot(models.Model): return [] return [f"availability-person-{owner.pk}"] - ceil_offet_eps = 1e-4 + ceil_offet_eps = decimal.Decimal(1e-4) # self.slots_in_an_hour is set in AKJSONExportView data = { -- GitLab From 693fec82a0f222f59773a298c13f3bf70c4bdb77 Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Tue, 21 Jan 2025 17:00:53 +0100 Subject: [PATCH 43/77] Fix import --- AKModel/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AKModel/models.py b/AKModel/models.py index 0e995932..f25b04de 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -1,9 +1,9 @@ +import decimal import itertools import json import math from dataclasses import dataclass from datetime import datetime, timedelta -from decimal import Decimal from typing import Iterable, Generator from django.db import models -- GitLab From 57ac0fe7a353a074d82d1d5be58055e914a4a53b Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Tue, 21 Jan 2025 19:50:18 +0100 Subject: [PATCH 44/77] Add new field to AK info block with exact duration in hours --- AKModel/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/AKModel/models.py b/AKModel/models.py index f25b04de..08607e19 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -952,6 +952,7 @@ class AKSlot(models.Model): for owner in self.ak.owners.all()]), "description": self.ak.description, "reso": self.ak.reso, + "duration_in_hours": self.duration, }, } -- GitLab From 9301ef7a672f9bd5cf4c59ac1264658658bb6e2c Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Tue, 21 Jan 2025 19:57:51 +0100 Subject: [PATCH 45/77] Convert decimal to float first --- AKModel/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AKModel/models.py b/AKModel/models.py index 08607e19..57e488fe 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -952,7 +952,7 @@ class AKSlot(models.Model): for owner in self.ak.owners.all()]), "description": self.ak.description, "reso": self.ak.reso, - "duration_in_hours": self.duration, + "duration_in_hours": float(self.duration), }, } -- GitLab From ba533efa2beb5d9c8b9cce7525a5bb99e782230c Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Wed, 22 Jan 2025 14:28:12 +0100 Subject: [PATCH 46/77] Export conflicts and dependencies as AK properties --- AKModel/models.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/AKModel/models.py b/AKModel/models.py index d30912d3..4c6d6188 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -934,11 +934,17 @@ class AKSlot(models.Model): return [] return [f"availability-person-{owner.pk}"] + conflict_slots = AKSlot.objects.filter(ak__in=self.ak.conflicts.all()) + dependency_slots = AKSlot.objects.filter(ak__in=self.ak.prerequisites.all()) + # self.slots_in_an_hour is set in AKJSONExportView data = { "id": str(self.pk), "duration": round(self.duration * self.slots_in_an_hour), - "properties": {}, + "properties": { + "conflicts": [str(conflict.pk) for conflict in conflict_slots.all()], + "dependencies": [str(dep.pk) for dep in dependency_slots.all()], + }, "room_constraints": [constraint.name for constraint in self.ak.requirements.all()], "time_constraints": ["resolution"] if self.ak.reso else [], -- GitLab From f1d4a9eeaa269d4839bfb672d518da7ceaaaf2e1 Mon Sep 17 00:00:00 2001 From: Felix Blanke <s6feblan@uni-bonn.de> Date: Wed, 22 Jan 2025 13:45:40 +0000 Subject: [PATCH 47/77] Encapsulate discretization in Event::discretize_timeslots --- AKModel/models.py | 19 ++++++++++++++++++- AKModel/views/ak.py | 8 ++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/AKModel/models.py b/AKModel/models.py index d30912d3..2a80cef7 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -382,6 +382,23 @@ class Event(models.Model): constraints=category_constraints, ) + def discretize_timeslots(self, *, slots_in_an_hour: float = 1.0) -> Iterable[TimeslotBlock]: + """"Choose discretization scheme. + + Uses default_time_slots if the event has any DefaultSlot, otherwise uniform_time_slots. + + :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 + """ + + if DefaultSlot.objects.filter(event=self).exists(): + # discretize default slots if they exists + yield from merge_blocks(self.default_time_slots(slots_in_an_hour=slots_in_an_hour)) + else: + yield from self.uniform_time_slots(slots_in_an_hour=slots_in_an_hour) + def schedule_from_json(self, schedule: str) -> None: """Load AK schedule from a json string. @@ -396,7 +413,7 @@ class Event(models.Model): timeslot_dict = { timeslot.idx: timeslot - for block in merge_blocks(self.default_time_slots(slots_in_an_hour=slots_in_an_hour)) + for block in self.discretize_timeslots(slots_in_an_hour=slots_in_an_hour) for timeslot in block } diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index 90599a1b..1d60717b 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -9,7 +9,7 @@ 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, DefaultSlot, Event, AKOrgaMessage, AK, Room, AKOwner, merge_blocks +from AKModel.models import AKRequirement, AKSlot, Event, AKOrgaMessage, AK, Room, AKOwner class AKRequirementOverview(AdminViewMixin, FilterByEventSlugMixin, ListView): @@ -114,11 +114,7 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): if (values := AKSlot.objects.select_related().filter(ak__pk=ak_id, fixed=True)).exists() } - if DefaultSlot.objects.filter(event=self.event).exists(): - # discretize default slots if they exists - blocks = merge_blocks(self.event.default_time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR)) - else: - blocks = self.event.uniform_time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR) + blocks = self.event.discretize_timeslots(slots_in_an_hour=SLOTS_IN_AN_HOUR) for block in blocks: current_block = [] -- GitLab From c420f00ee20e109c4dd7b877e1cc041c98e5eb46 Mon Sep 17 00:00:00 2001 From: Lorax66 <lorenzo.conti@uni-bonn.de> Date: Wed, 22 Jan 2025 14:57:11 +0100 Subject: [PATCH 48/77] fix --- AKModel/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/AKModel/models.py b/AKModel/models.py index ef7da9a2..267f0bb0 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -966,7 +966,6 @@ class AKSlot(models.Model): "conflicts": [str(conflict.pk) for conflict in conflict_slots.all()], "dependencies": [str(dep.pk) for dep in dependency_slots.all()], }, - "properties": {}, "room_constraints": [constraint.name for constraint in self.ak.requirements.all()], "time_constraints": ["resolution"] if self.ak.reso else [], -- GitLab From 417e8151154d2848112fda9374fb2bb25b9c1857 Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Thu, 23 Jan 2025 16:55:06 +0100 Subject: [PATCH 49/77] Add other slots of same AK to conflict list --- AKModel/models.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/AKModel/models.py b/AKModel/models.py index 267f0bb0..7cca95c1 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -955,6 +955,7 @@ class AKSlot(models.Model): conflict_slots = AKSlot.objects.filter(ak__in=self.ak.conflicts.all()) dependency_slots = AKSlot.objects.filter(ak__in=self.ak.prerequisites.all()) + other_ak_slots = AKSlot.objects.filter(ak=self.ak).exclude(pk=self.pk) ceil_offet_eps = decimal.Decimal(1e-4) @@ -963,7 +964,9 @@ class AKSlot(models.Model): "id": str(self.pk), "duration": math.ceil(self.duration * self.slots_in_an_hour - ceil_offet_eps), "properties": { - "conflicts": [str(conflict.pk) for conflict in conflict_slots.all()], + "conflicts": + [str(conflict.pk) for conflict in conflict_slots.all()] + + [str(second_slot.pk) for second_slot in other_ak_slots.all()], "dependencies": [str(dep.pk) for dep in dependency_slots.all()], }, "room_constraints": [constraint.name -- GitLab From b0f0b9ae7e7082c0c057e99d3fe2f2f4120b3a71 Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Thu, 23 Jan 2025 16:55:26 +0100 Subject: [PATCH 50/77] Add django AK id to info dict of slot --- AKModel/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/AKModel/models.py b/AKModel/models.py index 7cca95c1..1d12a76d 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -979,6 +979,7 @@ class AKSlot(models.Model): "description": self.ak.description, "reso": self.ak.reso, "duration_in_hours": float(self.duration), + "django_ak_id": str(self.ak.pk), }, } -- GitLab From 57c40a50b029a5b817e3ae46694dbaf48667ef9b Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Sun, 26 Jan 2025 23:10:53 +0100 Subject: [PATCH 51/77] Refactor --- AKModel/models.py | 10 ++++---- AKModel/views/ak.py | 57 ++++++++++++++++++++++++--------------------- 2 files changed, 36 insertions(+), 31 deletions(-) diff --git a/AKModel/models.py b/AKModel/models.py index 267f0bb0..b645c0c1 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -943,10 +943,12 @@ class AKSlot(models.Model): # 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: + if self.fixed and self.start is not None: + ak_time_constraints = [f"fixed-akslot-{self.id}"] + elif Availability.is_event_covered(self.event, self.ak.availabilities.all()): ak_time_constraints = [f"availability-ak-{self.ak.pk}"] + else: + ak_time_constraints = [] def _owner_time_constraints(owner: AKOwner): if Availability.is_event_covered(self.event, owner.availabilities.all()): @@ -987,7 +989,7 @@ class AKSlot(models.Model): category_constraints = AKCategory.create_category_constraints([self.ak.category]) data["time_constraints"].extend(category_constraints) - if self.room is not None and self.fixed: + if self.fixed and self.room is not None: data["room_constraints"].append(f"availability-room-{self.room.pk}") if not any(constr.startswith("proxy") for constr in data["room_constraints"]): diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index 1d60717b..d266e9bc 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -59,13 +59,13 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): """Test if event is not covered by availabilities.""" return not Availability.is_event_covered(self.event, availabilities) - def _test_ak_fixed_in_slot(self, ak_id, slot: Availability, ak_fixed: dict) -> bool: - """Test if AK defined by `ak_id` is fixed to happen during slot.""" - if not ak_id in ak_fixed: + def _test_akslot_fixed_in_timeslot(self, ak_slot: AKSlot, timeslot: Availability) -> bool: + """Test if an AKSlot is fixed to overlap a timeslot slot.""" + if not ak_slot.fixed or ak_slot.start is None: 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) + fixed_avail = Availability(event=self.event, start=ak_slot.start, end=ak_slot.end) + return fixed_avail.overlaps(timeslot, strict=True) def _test_add_constraint(self, slot: Availability, availabilities: List[Availability]) -> bool: """Test if object is not available for whole event and may happen during slot.""" @@ -74,6 +74,19 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): and self._test_slot_contained(slot, availabilities) ) + def _generate_time_constraints( + self, + avail_label: str, + avail_dict: dict, + timeslot_avail: Availability, + prefix: str = "availability", + ) -> list[str]: + return [ + f"{prefix}-{avail_label}-{pk}" + for pk, availabilities in avail_dict.items() + if self._test_add_constraint(timeslot_avail, availabilities) + ] + def get_queryset(self): return super().get_queryset().order_by("ak__track") @@ -108,12 +121,6 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): 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() - } - blocks = self.event.discretize_timeslots(slots_in_an_hour=SLOTS_IN_AN_HOUR) for block in blocks: @@ -127,26 +134,22 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): time_constraints.append("resolution") # add fulfilled time constraints for all AKs that cannot happen during full event - 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_ak_fixed_in_slot(ak_id, timeslot.avail, ak_fixed) - ) - ]) + self._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([ - f"availability-person-{person_id}" - for person_id, availabilities in person_availabilities.items() - if self._test_add_constraint(timeslot.avail, availabilities) - ]) + self._generate_time_constraints("person", person_availabilities, timeslot.avail) + # add fulfilled time constraints for all rooms that are not available for full event + self._generate_time_constraints("room", room_availabilities, timeslot.avail) + + # add fulfilled time constraints for all AKSlots fixed to happen during timeslot time_constraints.extend([ - f"availability-room-{room_id}" - for room_id, availabilities in room_availabilities.items() - if self._test_add_constraint(timeslot.avail, availabilities) + f"fixed-akslot-{slot.id}" + for slot in AKSlot.objects.filter(event=self.event, fixed=True) + .exclude(start__isnull=True) + if self._test_akslot_fixed_in_timeslot(slot, timeslot.avail) ]) + time_constraints.extend(timeslot.constraints) current_block.append({ -- GitLab From cf2637d26115e30ed2e2ca10c919f45f14620ade Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Sun, 26 Jan 2025 23:29:04 +0100 Subject: [PATCH 52/77] Rename constraint for fixed rooms --- AKModel/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AKModel/models.py b/AKModel/models.py index b645c0c1..895523fd 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -826,7 +826,7 @@ class Room(models.Model): "time_constraints": time_constraints } - data["fulfilled_room_constraints"].append(f"availability-room-{self.pk}") + data["fulfilled_room_constraints"].append(f"fixed-room-{self.pk}") if not any(constr.startswith("proxy") for constr in data["fulfilled_room_constraints"]): data["fulfilled_room_constraints"].append("no-proxy") @@ -990,7 +990,7 @@ class AKSlot(models.Model): data["time_constraints"].extend(category_constraints) if self.fixed and self.room is not None: - data["room_constraints"].append(f"availability-room-{self.room.pk}") + data["room_constraints"].append(f"fixed-room-{self.room.pk}") if not any(constr.startswith("proxy") for constr in data["room_constraints"]): data["room_constraints"].append("no-proxy") -- GitLab From e3bdd34795a3c1b926bf568e17c05e8f21810082 Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Mon, 27 Jan 2025 00:19:20 +0100 Subject: [PATCH 53/77] Only add constr if event is not covered --- AKModel/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AKModel/models.py b/AKModel/models.py index 895523fd..ea5461bf 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -945,7 +945,7 @@ class AKSlot(models.Model): if self.fixed and self.start is not None: ak_time_constraints = [f"fixed-akslot-{self.id}"] - elif Availability.is_event_covered(self.event, self.ak.availabilities.all()): + elif not Availability.is_event_covered(self.event, self.ak.availabilities.all()): ak_time_constraints = [f"availability-ak-{self.ak.pk}"] else: ak_time_constraints = [] -- GitLab From 205436f871305b7071729f33609be28137fe83cc Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Mon, 27 Jan 2025 00:34:48 +0100 Subject: [PATCH 54/77] Iterate over AKs instead of AKSlots --- AKModel/views/ak.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index d266e9bc..caee99b4 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -109,8 +109,8 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): 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"] + ak.pk: Availability.union(ak.availabilities.all()) + for ak in AK.objects.filter(event=self.event).all() } room_availabilities = { room.pk: Availability.union(room.availabilities.all()) -- GitLab From 26f432ad285d715fea550ac09b4b868a9d4deccb Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Mon, 27 Jan 2025 00:35:29 +0100 Subject: [PATCH 55/77] Add generated constraints to list --- AKModel/views/ak.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index caee99b4..5e642dd5 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -134,13 +134,19 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): time_constraints.append("resolution") # add fulfilled time constraints for all AKs that cannot happen during full event - self._generate_time_constraints("ak", ak_availabilities, timeslot.avail) + time_constraints.extend( + self._generate_time_constraints("ak", ak_availabilities, timeslot.avail) + ) # add fulfilled time constraints for all persons that are not available for full event - self._generate_time_constraints("person", person_availabilities, timeslot.avail) + time_constraints.extend( + self._generate_time_constraints("person", person_availabilities, timeslot.avail) + ) # add fulfilled time constraints for all rooms that are not available for full event - self._generate_time_constraints("room", room_availabilities, timeslot.avail) + time_constraints.extend( + self._generate_time_constraints("room", room_availabilities, timeslot.avail) + ) # add fulfilled time constraints for all AKSlots fixed to happen during timeslot time_constraints.extend([ -- GitLab From e030e675181725f1d5600ca079f156e8962adcbf Mon Sep 17 00:00:00 2001 From: Felix Blanke <s6feblan@uni-bonn.de> Date: Thu, 6 Feb 2025 12:00:27 +0000 Subject: [PATCH 56/77] Avoid setting the slot duration at import --- AKModel/locale/de_DE/LC_MESSAGES/django.po | 417 ++++++++++-------- AKModel/models.py | 60 ++- AKModel/views/manage.py | 14 +- .../locale/de_DE/LC_MESSAGES/django.po | 18 +- .../locale/de_DE/LC_MESSAGES/django.po | 6 +- 5 files changed, 321 insertions(+), 194 deletions(-) diff --git a/AKModel/locale/de_DE/LC_MESSAGES/django.po b/AKModel/locale/de_DE/LC_MESSAGES/django.po index 4fcbd8a8..42b38565 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-05-31 14:22+0000\n" +"POT-Creation-Date: 2025-01-22 19:00+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,7 +33,8 @@ msgstr "Plan veröffentlichen" msgid "Unpublish plan" msgstr "Plan verbergen" -#: AKModel/admin.py:168 AKModel/models.py:612 AKModel/models.py:1028 +#: AKModel/admin.py:168 AKModel/models.py:689 AKModel/models.py:1114 +#: AKModel/models.py:1150 #: AKModel/templates/admin/AKModel/status/event_aks.html:10 #: AKModel/views/manage.py:75 AKModel/views/status.py:97 msgid "AKs" @@ -59,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:320 AKModel/views/ak.py:226 +#: AKModel/admin.py:320 AKModel/views/ak.py:237 msgid "Reset interest in AKs" msgstr "Interesse an AKs zurücksetzen" -#: AKModel/admin.py:330 AKModel/views/ak.py:241 +#: AKModel/admin.py:330 AKModel/views/ak.py:252 msgid "Reset AKs' interest counters" msgstr "Interessenszähler der AKs zurücksetzen" @@ -108,17 +109,18 @@ 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: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 +#: AKModel/availability/models.py:43 AKModel/models.py:160 +#: AKModel/models.py:489 AKModel/models.py:566 AKModel/models.py:599 +#: AKModel/models.py:625 AKModel/models.py:679 AKModel/models.py:821 +#: AKModel/models.py:897 AKModel/models.py:1053 AKModel/models.py:1110 +#: AKModel/models.py:1301 msgid "Event" msgstr "Event" -#: 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 +#: AKModel/availability/models.py:44 AKModel/models.py:490 +#: AKModel/models.py:567 AKModel/models.py:600 AKModel/models.py:626 +#: AKModel/models.py:680 AKModel/models.py:822 AKModel/models.py:898 +#: AKModel/models.py:1054 AKModel/models.py:1111 AKModel/models.py:1302 msgid "Associated event" msgstr "Zugehöriges Event" @@ -130,8 +132,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:748 -#: AKModel/models.py:810 AKModel/models.py:1034 +#: AKModel/availability/models.py:61 AKModel/models.py:825 +#: AKModel/models.py:887 AKModel/models.py:1120 msgid "Room" msgstr "Raum" @@ -139,8 +141,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:611 -#: AKModel/models.py:809 AKModel/models.py:962 +#: AKModel/availability/models.py:70 AKModel/models.py:688 +#: AKModel/models.py:886 AKModel/models.py:1048 msgid "AK" msgstr "AK" @@ -148,8 +150,8 @@ msgstr "AK" msgid "AK whose availability this is" msgstr "Verfügbarkeiten" -#: AKModel/availability/models.py:79 AKModel/models.py:493 -#: AKModel/models.py:1040 +#: AKModel/availability/models.py:79 AKModel/models.py:570 +#: AKModel/models.py:1126 msgid "AK Category" msgstr "AK-Kategorie" @@ -157,7 +159,7 @@ msgstr "AK-Kategorie" msgid "AK Category whose availability this is" msgstr "AK-Kategorie, deren Verfügbarkeit hier abgebildet wird" -#: AKModel/availability/models.py:300 +#: AKModel/availability/models.py:300 AKModel/models.py:721 msgid "Availabilities" msgstr "Verfügbarkeiten" @@ -219,7 +221,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:1209 +#: AKModel/forms.py:189 AKModel/models.py:1295 msgid "Default Slots" msgstr "Standardslots" @@ -266,7 +268,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:60 +#: AKModel/metaviews/admin.py:156 AKModel/models.py:129 msgid "Start" msgstr "Start" @@ -291,66 +293,66 @@ msgstr "Aktivieren?" msgid "Finish" msgstr "Abschluss" -#: AKModel/models.py:51 AKModel/models.py:481 AKModel/models.py:519 -#: AKModel/models.py:546 AKModel/models.py:564 AKModel/models.py:736 +#: AKModel/models.py:120 AKModel/models.py:558 AKModel/models.py:596 +#: AKModel/models.py:623 AKModel/models.py:641 AKModel/models.py:813 msgid "Name" msgstr "Name" -#: AKModel/models.py:52 +#: AKModel/models.py:121 msgid "Name or iteration of the event" msgstr "Name oder Iteration des Events" -#: AKModel/models.py:53 +#: AKModel/models.py:122 msgid "Short Form" msgstr "Kurzer Name" -#: AKModel/models.py:54 +#: AKModel/models.py:123 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:56 +#: AKModel/models.py:125 msgid "Place" msgstr "Ort" -#: AKModel/models.py:57 +#: AKModel/models.py:126 msgid "City etc. the event takes place in" msgstr "Stadt o.ä. in der das Event stattfindet" -#: AKModel/models.py:59 +#: AKModel/models.py:128 msgid "Time Zone" msgstr "Zeitzone" -#: AKModel/models.py:59 +#: AKModel/models.py:128 msgid "Time Zone where this event takes place in" msgstr "Zeitzone in der das Event stattfindet" -#: AKModel/models.py:60 +#: AKModel/models.py:129 msgid "Time the event begins" msgstr "Zeit zu der das Event beginnt" -#: AKModel/models.py:61 +#: AKModel/models.py:130 msgid "End" msgstr "Ende" -#: AKModel/models.py:61 +#: AKModel/models.py:130 msgid "Time the event ends" msgstr "Zeit zu der das Event endet" -#: AKModel/models.py:62 +#: AKModel/models.py:131 msgid "Resolution Deadline" msgstr "Resolutionsdeadline" -#: AKModel/models.py:63 +#: AKModel/models.py:132 msgid "When should AKs with intention to submit a resolution be done?" msgstr "Wann sollen AKs mit Resolutionsabsicht stattgefunden haben?" -#: AKModel/models.py:65 +#: AKModel/models.py:134 msgid "Interest Window Start" msgstr "Beginn Interessensbekundung" -#: AKModel/models.py:67 +#: AKModel/models.py:136 msgid "" "Opening time for expression of interest. When left blank, no interest " "indication will be possible." @@ -358,71 +360,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:69 +#: AKModel/models.py:138 msgid "Interest Window End" msgstr "Ende Interessensbekundung" -#: AKModel/models.py:70 +#: AKModel/models.py:139 msgid "Closing time for expression of interest." msgstr "Öffnungszeitpunkt für die Angabe von Interesse an AKs." -#: AKModel/models.py:72 +#: AKModel/models.py:141 msgid "Public event" msgstr "Öffentliches Event" -#: AKModel/models.py:73 +#: AKModel/models.py:142 msgid "Show this event on overview page." msgstr "Zeige dieses Event auf der Übersichtseite an" -#: AKModel/models.py:75 +#: AKModel/models.py:144 msgid "Active State" msgstr "Aktiver Status" -#: AKModel/models.py:75 +#: AKModel/models.py:144 msgid "Marks currently active events" msgstr "Markiert aktuell aktive Events" -#: AKModel/models.py:76 +#: AKModel/models.py:145 msgid "Plan Hidden" msgstr "Plan verborgen" -#: AKModel/models.py:76 +#: AKModel/models.py:145 msgid "Hides plan for non-staff users" msgstr "Verbirgt den Plan für Nutzer*innen ohne erweiterte Rechte" -#: AKModel/models.py:78 +#: AKModel/models.py:147 msgid "Plan published at" msgstr "Plan veröffentlicht am/um" -#: AKModel/models.py:79 +#: AKModel/models.py:148 msgid "Timestamp at which the plan was published" msgstr "Zeitpunkt, zu dem der Plan veröffentlicht wurde" -#: AKModel/models.py:81 +#: AKModel/models.py:150 msgid "Base URL" msgstr "URL-Prefix" -#: AKModel/models.py:81 +#: AKModel/models.py:150 msgid "Prefix for wiki link construction" msgstr "Prefix für die automatische Generierung von Wiki-Links" -#: AKModel/models.py:82 +#: AKModel/models.py:151 msgid "Wiki Export Template Name" msgstr "Wiki-Export Templatename" -#: AKModel/models.py:83 +#: AKModel/models.py:152 msgid "Default Slot Length" msgstr "Standardslotlänge" -#: AKModel/models.py:84 +#: AKModel/models.py:153 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:86 +#: AKModel/models.py:155 msgid "Contact email address" msgstr "E-Mail Kontaktadresse" -#: AKModel/models.py:87 +#: AKModel/models.py:156 msgid "" "An email address that is displayed on every page and can be used for all " "kinds of questions" @@ -430,75 +432,107 @@ msgstr "" "Eine Mailadresse die auf jeder Seite angezeigt wird und für alle Arten von " "Fragen genutzt werden kann" -#: AKModel/models.py:92 +#: AKModel/models.py:161 msgid "Events" msgstr "Events" -#: AKModel/models.py:407 +#: AKModel/models.py:430 +#, 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:440 +#, python-brace-format +msgid "" +"Duration of AK {ak_name} assigned by solver ({solver_duration} hours) is " +"less than the duration required by the slot ({slot_duration} hours)" +msgstr "" +"Die dem AK {ak_name} vom Solver zugewiesene Dauer ({solver_duration} Stunden) ist " +"kürzer als die aktuell vorgesehene Dauer des Slots ({slot_duration} Stunden)" + +#: AKModel/models.py:454 +#, python-brace-format +msgid "" +"Fixed AK {ak_name} assigned by solver to room {solver_room} is fixed to room " +"{slot_room}" +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:465 +#, python-brace-format +msgid "" +"Fixed AK {ak_name} assigned by solver to start at {solver_start} is fixed to " +"start at {slot_start}" +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:484 msgid "Nickname" msgstr "Spitzname" -#: AKModel/models.py:407 +#: AKModel/models.py:484 msgid "Name to identify an AK owner by" msgstr "Name, durch den eine AK-Leitung identifiziert wird" -#: AKModel/models.py:408 +#: AKModel/models.py:485 msgid "Slug" msgstr "Slug" -#: AKModel/models.py:408 +#: AKModel/models.py:485 msgid "Slug for URL generation" msgstr "Slug für URL-Generierung" -#: AKModel/models.py:409 +#: AKModel/models.py:486 msgid "Institution" msgstr "Instutution" -#: AKModel/models.py:409 +#: AKModel/models.py:486 msgid "Uni etc." msgstr "Universität o.ä." -#: AKModel/models.py:410 AKModel/models.py:573 +#: AKModel/models.py:487 AKModel/models.py:650 msgid "Web Link" msgstr "Internet Link" -#: AKModel/models.py:410 +#: AKModel/models.py:487 msgid "Link to Homepage" msgstr "Link zu Homepage oder Webseite" -#: AKModel/models.py:416 AKModel/models.py:1033 +#: AKModel/models.py:493 AKModel/models.py:1119 msgid "AK Owner" msgstr "AK-Leitung" -#: AKModel/models.py:417 +#: AKModel/models.py:494 msgid "AK Owners" msgstr "AK-Leitungen" -#: AKModel/models.py:481 +#: AKModel/models.py:558 msgid "Name of the AK Category" msgstr "Name der AK-Kategorie" -#: AKModel/models.py:482 AKModel/models.py:520 +#: AKModel/models.py:559 AKModel/models.py:597 msgid "Color" msgstr "Farbe" -#: AKModel/models.py:482 AKModel/models.py:520 +#: AKModel/models.py:559 AKModel/models.py:597 msgid "Color for displaying" msgstr "Farbe für die Anzeige" -#: AKModel/models.py:483 AKModel/models.py:567 +#: AKModel/models.py:560 AKModel/models.py:644 msgid "Description" msgstr "Beschreibung" -#: AKModel/models.py:484 +#: AKModel/models.py:561 msgid "Short description of this AK Category" msgstr "Beschreibung der AK-Kategorie" -#: AKModel/models.py:485 +#: AKModel/models.py:562 msgid "Present by default" msgstr "Defaultmäßig präsentieren" -#: AKModel/models.py:486 +#: AKModel/models.py:563 msgid "" "Present AKs of this category by default if AK owner did not specify whether " "this AK should be presented?" @@ -506,132 +540,132 @@ msgstr "" "AKs dieser Kategorie standardmäßig vorstellen, wenn die Leitungen das für " "ihren AK nicht explizit spezifiziert haben?" -#: AKModel/models.py:494 +#: AKModel/models.py:571 msgid "AK Categories" msgstr "AK-Kategorien" -#: AKModel/models.py:519 +#: AKModel/models.py:596 msgid "Name of the AK Track" msgstr "Name des AK-Tracks" -#: AKModel/models.py:526 +#: AKModel/models.py:603 msgid "AK Track" msgstr "AK-Track" -#: AKModel/models.py:527 +#: AKModel/models.py:604 msgid "AK Tracks" msgstr "AK-Tracks" -#: AKModel/models.py:546 +#: AKModel/models.py:623 msgid "Name of the Requirement" msgstr "Name der Anforderung" -#: AKModel/models.py:552 AKModel/models.py:1037 +#: AKModel/models.py:629 AKModel/models.py:1123 msgid "AK Requirement" msgstr "AK-Anforderung" -#: AKModel/models.py:553 +#: AKModel/models.py:630 msgid "AK Requirements" msgstr "AK-Anforderungen" -#: AKModel/models.py:564 +#: AKModel/models.py:641 msgid "Name of the AK" msgstr "Name des AKs" -#: AKModel/models.py:565 +#: AKModel/models.py:642 msgid "Short Name" msgstr "Kurzer Name" -#: AKModel/models.py:566 +#: AKModel/models.py:643 msgid "Name displayed in the schedule" msgstr "Name zur Anzeige im AK-Plan" -#: AKModel/models.py:567 +#: AKModel/models.py:644 msgid "Description of the AK" msgstr "Beschreibung des AKs" -#: AKModel/models.py:569 +#: AKModel/models.py:646 msgid "Owners" msgstr "Leitungen" -#: AKModel/models.py:570 +#: AKModel/models.py:647 msgid "Those organizing the AK" msgstr "Menschen, die den AK organisieren und halten" -#: AKModel/models.py:573 +#: AKModel/models.py:650 msgid "Link to wiki page" msgstr "Link zur Wiki Seite" -#: AKModel/models.py:574 +#: AKModel/models.py:651 msgid "Protocol Link" msgstr "Protokolllink" -#: AKModel/models.py:574 +#: AKModel/models.py:651 msgid "Link to protocol" msgstr "Link zum Protokoll" -#: AKModel/models.py:576 +#: AKModel/models.py:653 msgid "Category" msgstr "Kategorie" -#: AKModel/models.py:577 +#: AKModel/models.py:654 msgid "Category of the AK" msgstr "Kategorie des AKs" -#: AKModel/models.py:578 +#: AKModel/models.py:655 msgid "Track" msgstr "Track" -#: AKModel/models.py:579 +#: AKModel/models.py:656 msgid "Track the AK belongs to" msgstr "Track zu dem der AK gehört" -#: AKModel/models.py:581 +#: AKModel/models.py:658 msgid "Resolution Intention" msgstr "Resolutionsabsicht" -#: AKModel/models.py:582 +#: AKModel/models.py:659 msgid "Intends to submit a resolution" msgstr "Beabsichtigt eine Resolution einzureichen" -#: AKModel/models.py:583 +#: AKModel/models.py:660 msgid "Present this AK" msgstr "AK präsentieren" -#: AKModel/models.py:584 +#: AKModel/models.py:661 msgid "Present results of this AK" msgstr "Die Ergebnisse dieses AKs vorstellen" -#: AKModel/models.py:586 AKModel/views/status.py:170 +#: AKModel/models.py:663 AKModel/models.py:716 AKModel/views/status.py:170 msgid "Requirements" msgstr "Anforderungen" -#: AKModel/models.py:587 +#: AKModel/models.py:664 msgid "AK's Requirements" msgstr "Anforderungen des AKs" -#: AKModel/models.py:589 +#: AKModel/models.py:666 msgid "Conflicting AKs" msgstr "AK-Konflikte" -#: AKModel/models.py:590 +#: AKModel/models.py:667 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:591 +#: AKModel/models.py:668 msgid "Prerequisite AKs" msgstr "Vorausgesetzte AKs" -#: AKModel/models.py:592 +#: AKModel/models.py:669 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:594 +#: AKModel/models.py:671 msgid "Organizational Notes" msgstr "Notizen zur Organisation" -#: AKModel/models.py:595 +#: AKModel/models.py:672 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/" @@ -641,295 +675,303 @@ msgstr "" "Anmerkungen bitte den Button für Direktnachrichten verwenden (nach dem " "Anlegen/Bearbeiten)." -#: AKModel/models.py:598 +#: AKModel/models.py:675 AKModel/models.py:714 msgid "Interest" msgstr "Interesse" -#: AKModel/models.py:598 +#: AKModel/models.py:675 msgid "Expected number of people" msgstr "Erwartete Personenzahl" -#: AKModel/models.py:599 +#: AKModel/models.py:676 msgid "Interest Counter" msgstr "Interessenszähler" -#: AKModel/models.py:600 +#: AKModel/models.py:677 msgid "People who have indicated interest online" msgstr "Anzahl Personen, die online Interesse bekundet haben" -#: AKModel/models.py:605 +#: AKModel/models.py:682 msgid "Export?" msgstr "Export?" -#: AKModel/models.py:606 +#: AKModel/models.py:683 msgid "Include AK in wiki export?" msgstr "AK bei Wiki-Export berücksichtigen?" -#: AKModel/models.py:736 +#: AKModel/models.py:718 +msgid "Conflicts" +msgstr "Konflikte" + +#: AKModel/models.py:720 +msgid "Prerequisites" +msgstr "Voraussetzungen" + +#: AKModel/models.py:813 msgid "Name or number of the room" msgstr "Name oder Nummer des Raums" -#: AKModel/models.py:737 +#: AKModel/models.py:814 msgid "Location" msgstr "Ort" -#: AKModel/models.py:738 +#: AKModel/models.py:815 msgid "Name or number of the location" msgstr "Name oder Nummer des Ortes" -#: AKModel/models.py:739 +#: AKModel/models.py:816 msgid "Capacity" msgstr "Kapazität" -#: AKModel/models.py:740 +#: AKModel/models.py:817 msgid "Maximum number of people (-1 for unlimited)." msgstr "Maximale Personenzahl (-1 wenn unbeschränkt)." -#: AKModel/models.py:741 +#: AKModel/models.py:818 msgid "Properties" msgstr "Eigenschaften" -#: AKModel/models.py:742 +#: AKModel/models.py:819 msgid "AK requirements fulfilled by the room" msgstr "AK-Anforderungen, die dieser Raum erfüllt" -#: AKModel/models.py:749 AKModel/views/status.py:59 +#: AKModel/models.py:826 AKModel/views/status.py:59 msgid "Rooms" msgstr "Räume" -#: AKModel/models.py:809 +#: AKModel/models.py:886 msgid "AK being mapped" msgstr "AK, der zugeordnet wird" -#: AKModel/models.py:811 +#: AKModel/models.py:888 msgid "Room the AK will take place in" msgstr "Raum in dem der AK stattfindet" -#: AKModel/models.py:812 AKModel/models.py:1212 +#: AKModel/models.py:889 AKModel/models.py:1298 msgid "Slot Begin" msgstr "Beginn des Slots" -#: AKModel/models.py:812 AKModel/models.py:1212 +#: AKModel/models.py:889 AKModel/models.py:1298 msgid "Time and date the slot begins" msgstr "Zeit und Datum zu der der AK beginnt" -#: AKModel/models.py:814 +#: AKModel/models.py:891 msgid "Duration" msgstr "Dauer" -#: AKModel/models.py:815 +#: AKModel/models.py:892 msgid "Length in hours" msgstr "Länge in Stunden" -#: AKModel/models.py:817 +#: AKModel/models.py:894 msgid "Scheduling fixed" msgstr "Planung fix" -#: AKModel/models.py:818 +#: AKModel/models.py:895 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:823 +#: AKModel/models.py:900 msgid "Last update" msgstr "Letzte Aktualisierung" -#: AKModel/models.py:826 +#: AKModel/models.py:903 msgid "AK Slot" msgstr "AK-Slot" -#: AKModel/models.py:827 AKModel/models.py:1030 +#: AKModel/models.py:904 AKModel/models.py:1116 AKModel/models.py:1151 msgid "AK Slots" msgstr "AK-Slot" -#: AKModel/models.py:849 AKModel/models.py:858 +#: AKModel/models.py:926 AKModel/models.py:935 msgid "Not scheduled yet" msgstr "Noch nicht geplant" -#: AKModel/models.py:963 +#: AKModel/models.py:1049 msgid "AK this message belongs to" msgstr "AK zu dem die Nachricht gehört" -#: AKModel/models.py:964 +#: AKModel/models.py:1050 msgid "Message text" msgstr "Nachrichtentext" -#: AKModel/models.py:965 +#: AKModel/models.py:1051 msgid "Message to the organizers. This is not publicly visible." msgstr "" "Nachricht an die Organisator*innen. Diese ist nicht öffentlich sichtbar." -#: AKModel/models.py:969 +#: AKModel/models.py:1055 msgid "Resolved" msgstr "Erledigt" -#: AKModel/models.py:970 +#: AKModel/models.py:1056 msgid "This message has been resolved (no further action needed)" msgstr "" "Diese Nachricht wurde vollständig bearbeitet (keine weiteren Aktionen " "notwendig)" -#: AKModel/models.py:973 +#: AKModel/models.py:1059 msgid "AK Orga Message" msgstr "AK-Organachricht" -#: AKModel/models.py:974 +#: AKModel/models.py:1060 msgid "AK Orga Messages" msgstr "AK-Organachrichten" -#: AKModel/models.py:991 +#: AKModel/models.py:1077 msgid "Constraint Violation" msgstr "Constraintverletzung" -#: AKModel/models.py:992 +#: AKModel/models.py:1078 msgid "Constraint Violations" msgstr "Constraintverletzungen" -#: AKModel/models.py:999 +#: AKModel/models.py:1085 msgid "Owner has two parallel slots" msgstr "Leitung hat zwei Slots parallel" -#: AKModel/models.py:1000 +#: AKModel/models.py:1086 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:1001 +#: AKModel/models.py:1087 msgid "Room has two AK slots scheduled at the same time" msgstr "Raum hat zwei AK Slots gleichzeitig" -#: AKModel/models.py:1002 +#: AKModel/models.py:1088 msgid "Room does not satisfy the requirement of the scheduled AK" msgstr "Room erfüllt die Anforderungen des platzierten AKs nicht" -#: AKModel/models.py:1003 +#: AKModel/models.py:1089 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:1004 +#: AKModel/models.py:1090 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:1006 +#: AKModel/models.py:1092 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:1007 +#: AKModel/models.py:1093 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:1008 +#: AKModel/models.py:1094 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:1009 +#: AKModel/models.py:1095 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:1010 +#: AKModel/models.py:1096 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:1016 +#: AKModel/models.py:1102 msgid "Warning" msgstr "Warnung" -#: AKModel/models.py:1017 +#: AKModel/models.py:1103 msgid "Violation" msgstr "Verletzung" -#: AKModel/models.py:1019 +#: AKModel/models.py:1105 msgid "Type" msgstr "Art" -#: AKModel/models.py:1020 +#: AKModel/models.py:1106 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:1021 +#: AKModel/models.py:1107 msgid "Level" msgstr "Level" -#: AKModel/models.py:1022 +#: AKModel/models.py:1108 msgid "Severity level of the violation" msgstr "Schweregrad der Verletzung" -#: AKModel/models.py:1029 +#: AKModel/models.py:1115 msgid "AK(s) belonging to this constraint" msgstr "AK(s), die zu diesem Constraint gehören" -#: AKModel/models.py:1031 +#: AKModel/models.py:1117 msgid "AK Slot(s) belonging to this constraint" msgstr "AK Slot(s), die zu diesem Constraint gehören" -#: AKModel/models.py:1033 +#: AKModel/models.py:1119 msgid "AK Owner belonging to this constraint" msgstr "AK Leitung(en), die zu diesem Constraint gehören" -#: AKModel/models.py:1035 +#: AKModel/models.py:1121 msgid "Room belonging to this constraint" msgstr "Raum, der zu diesem Constraint gehört" -#: AKModel/models.py:1038 +#: AKModel/models.py:1124 msgid "AK Requirement belonging to this constraint" msgstr "AK Anforderung, die zu diesem Constraint gehört" -#: AKModel/models.py:1040 +#: AKModel/models.py:1126 msgid "AK Category belonging to this constraint" msgstr "AK Kategorie, di zu diesem Constraint gehört" -#: AKModel/models.py:1042 +#: AKModel/models.py:1128 msgid "Comment" msgstr "Kommentar" -#: AKModel/models.py:1042 +#: AKModel/models.py:1128 msgid "Comment or further details for this violation" msgstr "Kommentar oder weitere Details zu dieser Vereletzung" -#: AKModel/models.py:1045 +#: AKModel/models.py:1131 msgid "Timestamp" msgstr "Timestamp" -#: AKModel/models.py:1045 +#: AKModel/models.py:1131 msgid "Time of creation" msgstr "Zeitpunkt der ERstellung" -#: AKModel/models.py:1046 +#: AKModel/models.py:1132 msgid "Manually Resolved" msgstr "Manuell behoben" -#: AKModel/models.py:1047 +#: AKModel/models.py:1133 msgid "Mark this violation manually as resolved" msgstr "Markiere diese Verletzung manuell als behoben" -#: AKModel/models.py:1074 +#: AKModel/models.py:1160 #: AKModel/templates/admin/AKModel/requirements_overview.html:27 msgid "Details" msgstr "Details" -#: AKModel/models.py:1208 +#: AKModel/models.py:1294 msgid "Default Slot" msgstr "Standardslot" -#: AKModel/models.py:1213 +#: AKModel/models.py:1299 msgid "Slot End" msgstr "Ende des Slots" -#: AKModel/models.py:1213 +#: AKModel/models.py:1299 msgid "Time and date the slot ends" msgstr "Zeit und Datum zu der der Slot endet" -#: AKModel/models.py:1218 +#: AKModel/models.py:1304 msgid "Primary categories" msgstr "Primäre Kategorien" -#: AKModel/models.py:1219 +#: AKModel/models.py:1305 msgid "Categories that should be assigned to this slot primarily" msgstr "Kategorieren, die diesem Slot primär zugewiesen werden sollen" -#: AKModel/site.py:14 +#: AKModel/site.py:13 AKModel/site.py:14 msgid "Administration" msgstr "Verwaltung" @@ -1135,35 +1177,35 @@ msgstr "AK-CSV-Export" msgid "AK JSON Export" msgstr "AK-JSON-Export" -#: AKModel/views/ak.py:175 +#: AKModel/views/ak.py:186 msgid "AK Wiki Export" msgstr "AK-Wiki-Export" -#: AKModel/views/ak.py:186 AKModel/views/manage.py:55 +#: AKModel/views/ak.py:197 AKModel/views/manage.py:55 msgid "Wishes" msgstr "Wünsche" -#: AKModel/views/ak.py:198 +#: AKModel/views/ak.py:209 msgid "Delete AK Orga Messages" msgstr "AK-Organachrichten löschen" -#: AKModel/views/ak.py:216 +#: AKModel/views/ak.py:227 msgid "AK Orga Messages successfully deleted" msgstr "AK-Organachrichten erfolgreich gelöscht" -#: AKModel/views/ak.py:228 +#: AKModel/views/ak.py:239 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:229 +#: AKModel/views/ak.py:240 msgid "Reset of interest in AKs successful." msgstr "Interesse an AKs erfolgreich zurückgesetzt." -#: AKModel/views/ak.py:243 +#: AKModel/views/ak.py:254 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:244 +#: AKModel/views/ak.py:255 msgid "AKs' interest counters set back to 0." msgstr "Interessenszähler der AKs zurückgesetzt" @@ -1265,6 +1307,15 @@ msgstr "" msgid "AK Schedule JSON Import" msgstr "AK-Plan JSON-Import" +#: AKModel/views/manage.py:265 +#, python-brace-format +msgid "Successfully imported {n} slot(s)" +msgstr "Erfolgreich {n} Slot(s) importiert" + +#: AKModel/views/manage.py:271 +msgid "Importing an AK schedule failed! Reason: " +msgstr "AK-Plan importieren fehlgeschlagen! Grund: " + #: AKModel/views/room.py:37 #, python-format msgid "Created Room '%(room)s'" diff --git a/AKModel/models.py b/AKModel/models.py index c51b7556..1cd3f1ba 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -6,7 +6,7 @@ from dataclasses import dataclass from datetime import datetime, timedelta from typing import Iterable, Generator -from django.db import models +from django.db import models, transaction from django.apps import apps from django.db.models import Count from django.urls import reverse_lazy @@ -401,7 +401,8 @@ class Event(models.Model): else: yield from self.uniform_time_slots(slots_in_an_hour=slots_in_an_hour) - def schedule_from_json(self, schedule: str) -> None: + @transaction.atomic + def schedule_from_json(self, schedule: str) -> int: """Load AK schedule from a json string. :param schedule: A string that can be decoded to json, describing @@ -419,18 +420,63 @@ class Event(models.Model): for timeslot in block } + slots_updated = 0 for scheduled_slot in schedule["scheduled_aks"]: + scheduled_slot["timeslot_ids"] = list(map(int, scheduled_slot["timeslot_ids"])) 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"])) + 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) + ) start_timeslot = timeslot_dict[min(scheduled_slot["timeslot_ids"])].avail end_timeslot = timeslot_dict[max(scheduled_slot["timeslot_ids"])].avail + solver_duration = (end_timeslot.end - start_timeslot.start).total_seconds() / 3600.0 + + 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, + ) + ) + + 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, + ) + ) + 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, + ) + ) + else: + slot.room = Room.objects.get(id=int(scheduled_slot["room_id"])) + slot.start = start_timeslot.start + slot.save() + slots_updated += 1 - slot.start = start_timeslot.start - slot.duration = (end_timeslot.end - start_timeslot.start).total_seconds() / 3600.0 - slot.save() + return slots_updated class AKOwner(models.Model): """ An AKOwner describes the person organizing/holding an AK. diff --git a/AKModel/views/manage.py b/AKModel/views/manage.py index 1bad9534..3acb05fd 100644 --- a/AKModel/views/manage.py +++ b/AKModel/views/manage.py @@ -257,6 +257,18 @@ class AKScheduleJSONImportView(EventSlugMixin, IntermediateAdminView): title = _("AK Schedule JSON Import") def form_valid(self, form): - self.event.schedule_from_json(form.data["json_data"]) + try: + number_of_slots_changed = self.event.schedule_from_json(form.data["json_data"]) + messages.add_message( + self.request, + messages.SUCCESS, + _("Successfully imported {n} slot(s)").format(n=number_of_slots_changed) + ) + except ValueError as ex: + messages.add_message( + self.request, + messages.ERROR, + _("Importing an AK schedule failed! Reason: ") + str(ex), + ) return redirect("admin:event_status", self.event.slug) diff --git a/AKScheduling/locale/de_DE/LC_MESSAGES/django.po b/AKScheduling/locale/de_DE/LC_MESSAGES/django.po index e66d8d55..1e391c1f 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: 2024-04-25 00:24+0200\n" +"POT-Creation-Date: 2025-01-22 19:00+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" @@ -27,7 +27,7 @@ msgstr "Ende" #: AKScheduling/forms.py:26 msgid "Duration" -msgstr "" +msgstr "Dauer" #: AKScheduling/forms.py:27 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:171 @@ -107,6 +107,7 @@ msgid "Event Status" msgstr "Event-Status" #: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:113 +#: AKScheduling/views.py:48 msgid "Scheduling" msgstr "Scheduling" @@ -239,6 +240,7 @@ msgstr[1] "" " " #: AKScheduling/templates/admin/AKScheduling/unscheduled.html:7 +#: AKScheduling/views.py:25 msgid "Unscheduled AK Slots" msgstr "Noch nicht geschedulte AK-Slots" @@ -246,10 +248,22 @@ msgstr "Noch nicht geschedulte AK-Slots" msgid "Count" msgstr "Anzahl" +#: AKScheduling/views.py:89 +msgid "Constraint violations for" +msgstr "Constraintverletzungen für" + +#: AKScheduling/views.py:104 +msgid "AKs requiring special attention for" +msgstr "AKs die besondere Aufmerksamkeit erfordern für" + #: AKScheduling/views.py:150 msgid "Interest updated" msgstr "Interesse aktualisiert" +#: AKScheduling/views.py:157 +msgid "Enter interest" +msgstr "Interesse eingeben" + #: AKScheduling/views.py:201 msgid "Wishes" msgstr "Wünsche" diff --git a/AKSubmission/locale/de_DE/LC_MESSAGES/django.po b/AKSubmission/locale/de_DE/LC_MESSAGES/django.po index e8add44b..e524139c 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: 2024-05-27 01:57+0000\n" +"POT-Creation-Date: 2025-01-22 19:00+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" @@ -418,6 +418,10 @@ msgstr "" msgid "Currently planned AKs" msgstr "Aktuell geplante AKs" +#: AKSubmission/views.py:231 +msgid "AKs with Track" +msgstr "AK mit Track" + #: AKSubmission/views.py:300 msgid "Event inactive. Cannot create or update." msgstr "Event inaktiv. Hinzufügen/Bearbeiten nicht möglich." -- GitLab From 7140368303f5bb94bc3278f663fb7a731c4887ba Mon Sep 17 00:00:00 2001 From: Felix Blanke <s6feblan@uni-bonn.de> Date: Thu, 6 Feb 2025 12:43:45 +0000 Subject: [PATCH 57/77] Add tests on json export --- .gitlab-ci.yml | 4 +- AKDashboard/tests.py | 2 +- AKModel/availability/models.py | 13 +- AKModel/fixtures/model.json | 191 +++++- AKModel/tests/__init__.py | 0 AKModel/tests/test_json_export.py | 777 ++++++++++++++++++++++ AKModel/{tests.py => tests/test_views.py} | 203 ++++-- AKModel/views/ak.py | 11 +- AKPlan/tests.py | 2 +- AKScheduling/api.py | 4 +- AKScheduling/models.py | 4 +- AKScheduling/tests.py | 2 +- AKSubmission/tests.py | 2 +- 13 files changed, 1136 insertions(+), 79 deletions(-) create mode 100644 AKModel/tests/__init__.py create mode 100644 AKModel/tests/test_json_export.py rename AKModel/{tests.py => tests/test_views.py} (60%) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 55ef5901..5855c87c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -38,7 +38,7 @@ test: 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 + - pip install pytest-cov unittest-xml-reporting beautifulsoup4 - coverage run --source='.' manage.py test --settings AKPlanning.settings_ci after_script: - source venv/bin/activate @@ -56,6 +56,8 @@ 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 f96af9a1..de25ffbc 100644 --- a/AKDashboard/tests.py +++ b/AKDashboard/tests.py @@ -6,7 +6,7 @@ from django.utils.timezone import now from AKDashboard.models import DashboardButton from AKModel.models import Event, AK, AKCategory -from AKModel.tests import BasicViewTests +from AKModel.tests.test_views import BasicViewTests class DashboardTests(TestCase): diff --git a/AKModel/availability/models.py b/AKModel/availability/models.py index 27a6c228..e2a64b22 100644 --- a/AKModel/availability/models.py +++ b/AKModel/availability/models.py @@ -280,6 +280,16 @@ class Availability(models.Model): return Availability(start=timeframe_start, end=timeframe_end, event=event, person=person, room=room, ak=ak, ak_category=ak_category) + def is_covered(self, availabilities: List['Availability']): + """Check if list of availibilities cover this object. + + :param availabilities: availabilities to check. + :return: whether the availabilities cover full event. + :rtype: bool + """ + avail_union = Availability.union(availabilities) + return any(avail.contains(self) for avail in avail_union) + @classmethod def is_event_covered(cls, event: Event, availabilities: List['Availability']) -> bool: """Check if list of availibilities cover whole event. @@ -292,8 +302,7 @@ class Availability(models.Model): # 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 any(avail.contains(full_event) for avail in avail_union) + return full_event.is_covered(availabilities) class Meta: verbose_name = _('Availability') diff --git a/AKModel/fixtures/model.json b/AKModel/fixtures/model.json index d848041d..42cf6c81 100644 --- a/AKModel/fixtures/model.json +++ b/AKModel/fixtures/model.json @@ -93,7 +93,7 @@ "model": "AKModel.akcategory", "pk": 1, "fields": { - "name": "Spa▀", + "name": "Spaß", "color": "275246", "description": "", "present_by_default": true, @@ -115,7 +115,7 @@ "model": "AKModel.akcategory", "pk": 3, "fields": { - "name": "Spa▀/Kultur", + "name": "Spaß/Kultur", "color": "333333", "description": "", "present_by_default": true, @@ -436,6 +436,62 @@ ] } }, +{ + "model": "AKModel.ak", + "pk": 4, + "fields": { + "name": "Test AK fixed slots", + "short_name": "testfixed", + "description": "", + "link": "", + "protocol_link": "", + "category": 4, + "track": null, + "reso": false, + "present": true, + "notes": "", + "interest": -1, + "interest_counter": 0, + "include_in_export": false, + "event": 2, + "owners": [ + 1 + ], + "requirements": [ + 3 + ], + "conflicts": [], + "prerequisites": [] + } +}, +{ + "model": "AKModel.ak", + "pk": 5, + "fields": { + "name": "Test AK Ernst", + "short_name": "testernst", + "description": "", + "link": "", + "protocol_link": "", + "category": 2, + "track": null, + "reso": false, + "present": true, + "notes": "", + "interest": -1, + "interest_counter": 0, + "include_in_export": false, + "event": 1, + "owners": [ + 3 + ], + "requirements": [ + 2 + ], + "conflicts": [], + "prerequisites": [] + } +}, { "model": "AKModel.room", "pk": 1, @@ -460,6 +516,19 @@ "properties": [] } }, +{ + "model": "AKModel.room", + "pk": 3, + "fields": { + "name": "BBB Session 1", + "location": "", + "capacity": -1, + "event": 1, + "properties": [ + 2 + ] + } +}, { "model": "AKModel.akslot", "pk": 1, @@ -525,6 +594,58 @@ "updated": "2022-12-02T12:23:11.856Z" } }, +{ + "model": "AKModel.akslot", + "pk": 6, + "fields": { + "ak": 4, + "room": null, + "start": "2020-11-08T18:30:00Z", + "duration": "2.00", + "fixed": true, + "event": 2, + "updated": "2022-12-02T12:23:11.856Z" + } +}, +{ + "model": "AKModel.akslot", + "pk": 7, + "fields": { + "ak": 4, + "room": 2, + "start": null, + "duration": "2.00", + "fixed": true, + "event": 2, + "updated": "2022-12-02T12:23:11.856Z" + } +}, +{ + "model": "AKModel.akslot", + "pk": 8, + "fields": { + "ak": 4, + "room": 2, + "start": "2020-11-07T16:00:00Z", + "duration": "2.00", + "fixed": true, + "event": 2, + "updated": "2022-12-02T12:23:11.856Z" + } +}, +{ + "model": "AKModel.akslot", + "pk": 9, + "fields": { + "ak": 5, + "room": null, + "start": null, + "duration": "2.00", + "fixed": false, + "event": 1, + "updated": "2022-12-02T12:23:11.856Z" + } +}, { "model": "AKModel.constraintviolation", "pk": 1, @@ -668,5 +789,71 @@ "start": "2020-11-07T18:30:00Z", "end": "2020-11-07T21:30:00Z" } +}, +{ + "model": "AKModel.availability", + "pk": 7, + "fields": { + "event": 1, + "person": null, + "room": null, + "ak": 5, + "ak_category": null, + "start": "2020-10-01T17:41:22Z", + "end": "2020-10-04T17:41:30Z" + } +}, +{ + "model": "AKModel.availability", + "pk": 8, + "fields": { + "event": 1, + "person": null, + "room": 3, + "ak": null, + "ak_category": null, + "start": "2020-10-01T17:41:22Z", + "end": "2020-10-04T17:41:30Z" + } +}, +{ + "model": "AKModel.defaultslot", + "pk": 1, + "fields": { + "event": 2, + "start": "2020-11-07T08:00:00Z", + "end": "2020-11-07T12:00:00Z", + "primary_categories": [5] + } +}, +{ + "model": "AKModel.defaultslot", + "pk": 2, + "fields": { + "event": 2, + "start": "2020-11-07T14:00:00Z", + "end": "2020-11-07T17:00:00Z", + "primary_categories": [4] + } +}, +{ + "model": "AKModel.defaultslot", + "pk": 3, + "fields": { + "event": 2, + "start": "2020-11-08T08:00:00Z", + "end": "2020-11-08T19:00:00Z", + "primary_categories": [4, 5] + } +}, +{ + "model": "AKModel.defaultslot", + "pk": 4, + "fields": { + "event": 2, + "start": "2020-11-09T17:00:00Z", + "end": "2020-11-10T01:00:00Z", + "primary_categories": [4, 5, 3] + } } ] diff --git a/AKModel/tests/__init__.py b/AKModel/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/AKModel/tests/test_json_export.py b/AKModel/tests/test_json_export.py new file mode 100644 index 00000000..d53cd4c5 --- /dev/null +++ b/AKModel/tests/test_json_export.py @@ -0,0 +1,777 @@ +import json +import math + +from collections import defaultdict +from collections.abc import Iterable +from datetime import datetime, timedelta +from itertools import chain + +from bs4 import BeautifulSoup +from django.contrib.auth import get_user_model +from django.test import TestCase +from django.urls import reverse + +from AKModel.availability.models import Availability +from AKModel.models import ( + Event, + AKOwner, + AKCategory, + AK, + Room, + AKSlot, + DefaultSlot, +) + + +class JSONExportTest(TestCase): + """Test if JSON export is correct. + + It tests if the output conforms to the KoMa specification: + https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format + """ + + fixtures = ["model.json"] + + @classmethod + def setUpTestData(cls): + """Shared set up by initializing admin user.""" + cls.admin_user = get_user_model().objects.create( + username="Test Admin User", + email="testadmin@example.com", + password="adminpw", + is_staff=True, + is_superuser=True, + is_active=True, + ) + + def setUp(self): + self.client.force_login(self.admin_user) + self.export_dict = {} + self.export_objects = { + "aks": {}, + "rooms": {}, + "participants": {}, + } + + self.ak_slots: Iterable[AKSlot] = [] + self.rooms: Iterable[Room] = [] + self.slots_in_an_hour: float = 1.0 + self.event: Event | None = None + + def set_up_event(self, event: Event) -> None: + """Set up by retrieving json export and initializing data.""" + + export_url = reverse("admin:ak_json_export", kwargs={"event_slug": event.slug}) + response = self.client.get(export_url) + + self.assertEqual(response.status_code, 200, "Export not working at all") + + soup = BeautifulSoup(response.content, features="lxml") + self.export_dict = json.loads(soup.find("pre").string) + + self.export_objects["aks"] = {ak["id"]: ak for ak in self.export_dict["aks"]} + self.export_objects["rooms"] = { + room["id"]: room for room in self.export_dict["rooms"] + } + self.export_objects["participants"] = { + participant["id"]: participant + for participant in self.export_dict["participants"] + } + + self.ak_slots = AKSlot.objects.filter(event__slug=event.slug).all() + self.rooms = Room.objects.filter(event__slug=event.slug).all() + self.slots_in_an_hour = 1 / self.export_dict["timeslots"]["info"]["duration"] + self.event = event + + def test_all_aks_exported(self): + """Test if exported AKs match AKSlots of Event.""" + for event in Event.objects.all(): + with self.subTest(event=event): + self.set_up_event(event=event) + self.assertEqual( + {str(slot.pk) for slot in self.ak_slots}, + self.export_objects["aks"].keys(), + "Exported AKs does not match the AKSlots of the event", + ) + + def _check_uniqueness(self, lst, name: str, key: str | None = "id"): + if key is not None: + lst = [entry[key] for entry in lst] + self.assertEqual(len(lst), len(set(lst)), f"{name} IDs not unique!") + + def _check_type(self, attr, cls, name: str, item: str) -> None: + self.assertTrue(isinstance(attr, cls), f"{item} {name} not a {cls}") + + def _check_lst( + self, lst: list[str], name: str, item: str, contained_type=str + ) -> None: + self.assertTrue(isinstance(lst, list), f"{item} {name} not a list") + self.assertTrue( + all(isinstance(c, contained_type) for c in lst), + f"{item} has non-{contained_type} {name}", + ) + if contained_type in {str, int}: + self._check_uniqueness(lst, name, key=None) + + def test_ak_conformity_to_spec(self): + """Test if AK JSON structure and types conform to standard.""" + for event in Event.objects.all(): + with self.subTest(event=event): + self.set_up_event(event=event) + + self._check_uniqueness(self.export_dict["aks"], "AK") + for ak in self.export_dict["aks"]: + item = f"AK {ak['id']}" + self.assertEqual( + ak.keys(), + { + "id", + "duration", + "properties", + "room_constraints", + "time_constraints", + "info", + }, + f"{item} keys not as expected", + ) + self.assertEqual( + ak["info"].keys(), + { + "name", + "head", + "description", + "reso", + "duration_in_hours", + "django_ak_id", + }, + f"{item} info keys not as expected", + ) + self.assertEqual( + ak["properties"].keys(), + {"conflicts", "dependencies"}, + f"{item} properties keys not as expected", + ) + + self._check_type(ak["id"], str, "id", item=item) + self._check_type(ak["duration"], int, "duration", item=item) + self._check_type(ak["info"]["name"], str, "info/name", item=item) + self._check_type(ak["info"]["head"], str, "info/head", item=item) + self._check_type( + ak["info"]["description"], str, "info/description", item=item + ) + self._check_type(ak["info"]["reso"], bool, "info/reso", item=item) + self._check_type( + ak["info"]["duration_in_hours"], + float, + "info/duration_in_hours", + item=item, + ) + self._check_type( + ak["info"]["django_ak_id"], + str, + "info/django_ak_id", + item=item, + ) + + self._check_lst( + ak["properties"]["conflicts"], "conflicts", item=item + ) + self._check_lst( + ak["properties"]["dependencies"], "dependencies", item=item + ) + self._check_lst( + ak["time_constraints"], "time_constraints", item=item + ) + self._check_lst( + ak["room_constraints"], "room_constraints", item=item + ) + + def test_room_conformity_to_spec(self): + """Test if Room JSON structure and types conform to standard.""" + for event in Event.objects.all(): + with self.subTest(event=event): + self.set_up_event(event=event) + + self._check_uniqueness(self.export_dict["rooms"], "Room") + for room in self.export_dict["rooms"]: + item = f"Room {room['id']}" + self.assertEqual( + room.keys(), + { + "id", + "info", + "capacity", + "fulfilled_room_constraints", + "time_constraints", + }, + f"{item} keys not as expected", + ) + self.assertEqual( + room["info"].keys(), + {"name"}, + f"{item} info keys not as expected", + ) + + self._check_type(room["id"], str, "id", item=item) + self._check_type(room["capacity"], int, "capacity", item=item) + self._check_type(room["info"]["name"], str, "info/name", item=item) + + self.assertTrue( + room["capacity"] > 0 or room["capacity"] == -1, + "invalid room capacity", + ) + + self._check_lst( + room["time_constraints"], "time_constraints", item=item + ) + self._check_lst( + room["fulfilled_room_constraints"], + "fulfilled_room_constraints", + item=item, + ) + + def test_timeslots_conformity_to_spec(self): + """Test if Timeslots JSON structure and types conform to standard.""" + for event in Event.objects.all(): + with self.subTest(event=event): + self.set_up_event(event=event) + + self._check_uniqueness( + chain.from_iterable(self.export_dict["timeslots"]["blocks"]), + "Timeslots", + ) + item = "timeslots" + self.assertEqual( + self.export_dict["timeslots"].keys(), + {"info", "blocks"}, + "timeslot keys not as expected", + ) + self.assertEqual( + self.export_dict["timeslots"]["info"].keys(), + {"duration"}, + "timeslot info keys not as expected", + ) + self._check_type( + self.export_dict["timeslots"]["info"]["duration"], + float, + "info/duration", + item=item, + ) + self._check_lst( + self.export_dict["timeslots"]["blocks"], + "blocks", + item=item, + contained_type=list, + ) + + prev_id = None + for timeslot in chain.from_iterable( + self.export_dict["timeslots"]["blocks"] + ): + item = f"timeslot {timeslot['id']}" + self.assertEqual( + timeslot.keys(), + {"id", "info", "fulfilled_time_constraints"}, + f"{item} keys not as expected", + ) + self.assertEqual( + timeslot["info"].keys(), + {"start", "end"}, + f"{item} info keys not as expected", + ) + self._check_type(timeslot["id"], str, "id", item=item) + self._check_type( + timeslot["info"]["start"], str, "info/start", item=item + ) + self._check_lst( + timeslot["fulfilled_time_constraints"], + "fulfilled_time_constraints", + item=item, + ) + + if prev_id is not None: + self.assertLess( + prev_id, + int(timeslot["id"]), + "timeslot ids must be increasing", + ) + prev_id = int(timeslot["id"]) + + def test_general_conformity_to_spec(self): + """Test if rest of JSON structure and types conform to standard.""" + for event in Event.objects.all(): + with self.subTest(event=event): + self.set_up_event(event=event) + + self.assertEqual( + self.export_dict["participants"], + [], + "Empty participant list expected", + ) + + info_keys = {"title": "name", "slug": "slug"} + for attr in ["contact_email", "place"]: + if hasattr(self.event, attr) and getattr(self.event, attr): + info_keys[attr] = attr + self.assertEqual( + self.export_dict["info"].keys(), + info_keys.keys(), + "info keys not as expected", + ) + for attr, attr_field in info_keys.items(): + self.assertEqual( + getattr(self.event, attr_field), self.export_dict["info"][attr] + ) + + self._check_uniqueness(self.export_dict["participants"], "Participants") + + def test_ak_durations(self): + """Test if all AK durations are correct.""" + for event in Event.objects.all(): + with self.subTest(event=event): + self.set_up_event(event=event) + + for slot in self.ak_slots: + ak = self.export_objects["aks"][str(slot.pk)] + + self.assertLessEqual( + float(slot.duration) * self.slots_in_an_hour - 1e-4, + ak["duration"], + "Slot duration is too short", + ) + + self.assertEqual( + math.ceil(float(slot.duration) * self.slots_in_an_hour - 1e-4), + ak["duration"], + "Slot duration is wrong", + ) + + self.assertEqual( + float(slot.duration), + ak["info"]["duration_in_hours"], + "Slot duration_in_hours is wrong", + ) + + def test_ak_conflicts(self): + """Test if all AK conflicts are correct.""" + for event in Event.objects.all(): + with self.subTest(event=event): + self.set_up_event(event=event) + + for slot in self.ak_slots: + ak = self.export_objects["aks"][str(slot.pk)] + conflict_slots = self.ak_slots.filter( + ak__in=slot.ak.conflicts.all() + ).values_list("pk", flat=True) + conflict_pks = {str(conflict_pk) for conflict_pk in conflict_slots} + + other_ak_slots = ( + self.ak_slots.filter(ak=slot.ak) + .exclude(pk=slot.pk) + .values_list("pk", flat=True) + ) + conflict_pks.update( + str(other_slot_pk) for other_slot_pk in other_ak_slots + ) + + self.assertEqual( + conflict_pks, + set(ak["properties"]["conflicts"]), + f"Conflicts for slot {slot.pk} not as expected", + ) + + def test_ak_depenedencies(self): + """Test if all AK dependencies are correct.""" + for event in Event.objects.all(): + with self.subTest(event=event): + self.set_up_event(event=event) + + for slot in self.ak_slots: + ak = self.export_objects["aks"][str(slot.pk)] + dependency_slots = self.ak_slots.filter( + ak__in=slot.ak.prerequisites.all() + ).values_list("pk", flat=True) + + self.assertEqual( + {str(dep_pk) for dep_pk in dependency_slots}, + set(ak["properties"]["dependencies"]), + f"Dependencies for slot {slot.pk} not as expected", + ) + + def test_ak_reso(self): + """Test if resolution intent of AKs is correctly exported.""" + for event in Event.objects.all(): + with self.subTest(event=event): + self.set_up_event(event=event) + + for slot in self.ak_slots: + ak = self.export_objects["aks"][str(slot.pk)] + self.assertEqual(slot.ak.reso, ak["info"]["reso"]) + self.assertEqual( + slot.ak.reso, "resolution" in ak["time_constraints"] + ) + + def test_ak_info(self): + """Test if contents of AK info dict is correct.""" + for event in Event.objects.all(): + with self.subTest(event=event): + self.set_up_event(event=event) + + for slot in self.ak_slots: + ak = self.export_objects["aks"][str(slot.pk)] + self.assertEqual(ak["info"]["name"], slot.ak.name) + self.assertEqual( + ak["info"]["head"], ", ".join(map(str, slot.ak.owners.all())) + ) + self.assertEqual(ak["info"]["description"], slot.ak.description) + self.assertEqual(ak["info"]["django_ak_id"], str(slot.ak.pk)) + + def test_ak_room_constraints(self): + """Test if AK room constraints are exported as expected.""" + for event in Event.objects.all(): + with self.subTest(event=event): + self.set_up_event(event=event) + + for slot in self.ak_slots: + ak = self.export_objects["aks"][str(slot.pk)] + requirements = list( + slot.ak.requirements.values_list("name", flat=True) + ) + + # proxy rooms + if not any(constr.startswith("proxy") for constr in requirements): + requirements.append("no-proxy") + + # fixed slot + if slot.fixed and slot.room is not None: + requirements.append(f"fixed-room-{slot.room.pk}") + + self.assertEqual( + set(ak["room_constraints"]), + set(requirements), + f"Room constraints for slot {slot.pk} not as expected", + ) + + def test_ak_time_constraints(self): + """Test if AK time constraints are exported as expected.""" + for event in Event.objects.all(): + with self.subTest(event=event): + self.set_up_event(event=event) + + for slot in self.ak_slots: + time_constraints = set() + + # add time constraints for AK category + if slot.ak.category: + category_constraints = AKCategory.create_category_constraints( + [slot.ak.category] + ) + time_constraints |= category_constraints + + if slot.fixed and slot.start is not None: + # fixed slot + time_constraints.add(f"fixed-akslot-{slot.pk}") + elif not Availability.is_event_covered( + slot.event, slot.ak.availabilities.all() + ): + # restricted AK availability + time_constraints.add(f"availability-ak-{slot.ak.pk}") + + for owner in slot.ak.owners.all(): + # restricted owner availability + if not Availability.is_event_covered( + slot.event, owner.availabilities.all() + ): + time_constraints.add(f"availability-person-{owner.pk}") + + ak = self.export_objects["aks"][str(slot.pk)] + self.assertEqual( + set(ak["time_constraints"]), + time_constraints, + f"Time constraints for slot {slot.pk} not as expected", + ) + + def test_all_rooms_exported(self): + """Test if exported Rooms match the rooms of Event.""" + for event in Event.objects.all(): + with self.subTest(event=event): + self.set_up_event(event=event) + + self.assertEqual( + {str(room.pk) for room in self.rooms}, + self.export_objects["rooms"].keys(), + "Exported Rooms do not match the Rooms of the event", + ) + + def test_room_capacity(self): + """Test if room capacity is exported correctly.""" + for event in Event.objects.all(): + with self.subTest(event=event): + self.set_up_event(event=event) + + for room in self.rooms: + export_room = self.export_objects["rooms"][str(room.pk)] + self.assertEqual(room.capacity, export_room["capacity"]) + + def test_room_info(self): + """Test if contents of Room info dict is correct.""" + for event in Event.objects.all(): + with self.subTest(event=event): + self.set_up_event(event=event) + + for room in self.rooms: + export_room = self.export_objects["rooms"][str(room.pk)] + self.assertEqual(room.name, export_room["info"]["name"]) + + def test_room_timeconstraints(self): + """Test if Room time constraints are exported as expected.""" + for event in Event.objects.all(): + with self.subTest(event=event): + self.set_up_event(event=event) + + for room in self.rooms: + time_constraints = set() + + # test if time availability of room is restricted + if not Availability.is_event_covered( + room.event, room.availabilities.all() + ): + time_constraints.add(f"availability-room-{room.pk}") + + export_room = self.export_objects["rooms"][str(room.pk)] + self.assertEqual( + time_constraints, set(export_room["time_constraints"]) + ) + + def test_room_fulfilledroomconstraints(self): + """Test if room constraints fulfilled by Room are correct.""" + for event in Event.objects.all(): + with self.subTest(event=event): + self.set_up_event(event=event) + + for room in self.rooms: + # room properties + fulfilled_room_constraints = set( + room.properties.values_list("name", flat=True) + ) + + # proxy rooms + if not any( + constr.startswith("proxy") + for constr in fulfilled_room_constraints + ): + fulfilled_room_constraints.add("no-proxy") + + fulfilled_room_constraints.add(f"fixed-room-{room.pk}") + + export_room = self.export_objects["rooms"][str(room.pk)] + self.assertEqual( + fulfilled_room_constraints, + set(export_room["fulfilled_room_constraints"]), + ) + + def _get_timeslot_start_end(self, timeslot): + start = datetime.strptime(timeslot["info"]["start"], "%Y-%m-%d %H:%M").replace( + tzinfo=self.event.timezone + ) + end = datetime.strptime(timeslot["info"]["end"], "%Y-%m-%d %H:%M").replace( + tzinfo=self.event.timezone + ) + return start, end + + def _get_cat_availability_in_export(self): + export_slot_cat_avails = defaultdict(list) + for timeslot in chain.from_iterable(self.export_dict["timeslots"]["blocks"]): + for constr in timeslot["fulfilled_time_constraints"]: + if constr.startswith("availability-cat-"): + cat_name = constr[len("availability-cat-") :] + start, end = self._get_timeslot_start_end(timeslot) + export_slot_cat_avails[cat_name].append( + Availability(event=self.event, start=start, end=end) + ) + return { + cat_name: Availability.union(avail_lst) + for cat_name, avail_lst in export_slot_cat_avails.items() + } + + def _get_cat_availability(self): + if DefaultSlot.objects.filter(event=self.event).exists(): + # Event has default slots -> use them for category availability + default_slots_avails = defaultdict(list) + for def_slot in DefaultSlot.objects.filter(event=self.event).all(): + avail = Availability( + event=self.event, + start=def_slot.start.astimezone(self.event.timezone), + end=def_slot.end.astimezone(self.event.timezone), + ) + for cat in def_slot.primary_categories.all(): + default_slots_avails[cat.name].append(avail) + + return { + cat_name: Availability.union(avail_lst) + for cat_name, avail_lst in default_slots_avails.items() + } + + # Event has no default slots -> all categories available through whole event + start = self.event.start.astimezone(self.event.timezone) + end = self.event.end.astimezone(self.event.timezone) + delta = (end - start).total_seconds() + + # tweak event end + # 1. shorten event to match discrete slot grid + slot_seconds = 3600 / self.slots_in_an_hour + remainder_seconds = delta % slot_seconds + remainder_seconds += 1 # add a second to compensate rounding errs + end -= timedelta(seconds=remainder_seconds) + + # set seconds and microseconds to 0 as they are not exported to the json + start -= timedelta(seconds=start.second, microseconds=start.microsecond) + end -= timedelta(seconds=end.second, microseconds=end.microsecond) + event_avail = Availability(event=self.event, start=start, end=end) + + category_names = AKCategory.objects.filter(event=self.event).values_list( + "name", flat=True + ) + return {cat_name: [event_avail] for cat_name in category_names} + + def test_timeslots_consecutive(self): + """Test if consecutive timeslots in JSON are in fact consecutive.""" + for event in Event.objects.all(): + with self.subTest(event=event): + self.set_up_event(event=event) + + prev_end = None + for timeslot in chain.from_iterable( + self.export_dict["timeslots"]["blocks"] + ): + start, end = self._get_timeslot_start_end(timeslot) + self.assertLess(start, end) + + delta = end - start + self.assertAlmostEqual( + delta.total_seconds() / (3600), 1 / self.slots_in_an_hour + ) + + if prev_end is not None: + self.assertLessEqual(prev_end, start) + prev_end = end + + def test_block_cover_categories(self): + """Test if blocks covers all default slot resp. whole event per category.""" + for event in Event.objects.all(): + with self.subTest(event=event): + self.set_up_event(event=event) + category_names = AKCategory.objects.filter(event=event).values_list( + "name", flat=True + ) + + export_cat_avails = self._get_cat_availability_in_export() + cat_avails = self._get_cat_availability() + + for cat_name in category_names: + for avail in cat_avails[cat_name]: + # check that all category availabilities are covered + self.assertTrue( + avail.is_covered(export_cat_avails[cat_name]), + f"AKCategory {cat_name}: avail ({avail.start} – {avail.end}) " + f"not covered by {[f'({a.start} – {a.end})' for a in export_cat_avails[cat_name]]}", + ) + + def _is_restricted_and_contained_slot( + self, slot: Availability, availabilities: list[Availability] + ) -> bool: + """Test if object is not available for whole event and may happen during slot.""" + return slot.is_covered(availabilities) and not Availability.is_event_covered( + self.event, availabilities + ) + + def _is_ak_fixed_in_slot( + self, + ak_slot: AKSlot, + timeslot_avail: Availability, + ) -> bool: + if not ak_slot.fixed or ak_slot.start is None: + return False + ak_slot_avail = Availability( + event=self.event, + start=ak_slot.start.astimezone(self.event.timezone), + end=ak_slot.end.astimezone(self.event.timezone), + ) + return timeslot_avail.overlaps(ak_slot_avail, strict=True) + + def test_timeslot_fulfilledconstraints(self): + """Test if fulfilled time constraints by timeslot are as expected.""" + for event in Event.objects.all(): + with self.subTest(event=event): + self.set_up_event(event=event) + + cat_avails = self._get_cat_availability() + for timeslot in chain.from_iterable( + self.export_dict["timeslots"]["blocks"] + ): + start, end = self._get_timeslot_start_end(timeslot) + timeslot_avail = Availability( + event=self.event, start=start, end=end + ) + + fulfilled_time_constraints = set() + + # reso deadline + if self.event.reso_deadline is not None: + # timeslot ends before deadline + if end < self.event.reso_deadline.astimezone( + self.event.timezone + ): + fulfilled_time_constraints.add("resolution") + + # add category constraints + fulfilled_time_constraints |= ( + AKCategory.create_category_constraints( + [ + cat + for cat in AKCategory.objects.filter( + event=self.event + ).all() + if timeslot_avail.is_covered(cat_avails[cat.name]) + ] + ) + ) + + # add owner constraints + fulfilled_time_constraints |= { + f"availability-person-{owner.id}" + for owner in AKOwner.objects.filter(event=self.event).all() + if self._is_restricted_and_contained_slot( + timeslot_avail, + Availability.union(owner.availabilities.all()), + ) + } + + # add room constraints + fulfilled_time_constraints |= { + f"availability-room-{room.id}" + for room in self.rooms + if self._is_restricted_and_contained_slot( + timeslot_avail, + Availability.union(room.availabilities.all()), + ) + } + + # add ak constraints + fulfilled_time_constraints |= { + f"availability-ak-{ak.id}" + for ak in AK.objects.filter(event=event) + if self._is_restricted_and_contained_slot( + timeslot_avail, Availability.union(ak.availabilities.all()) + ) + } + fulfilled_time_constraints |= { + f"fixed-akslot-{slot.id}" + for slot in self.ak_slots + if self._is_ak_fixed_in_slot(slot, timeslot_avail) + } + + self.assertEqual( + fulfilled_time_constraints, + set(timeslot["fulfilled_time_constraints"]), + ) diff --git a/AKModel/tests.py b/AKModel/tests/test_views.py similarity index 60% rename from AKModel/tests.py rename to AKModel/tests/test_views.py index 6730315d..63984745 100644 --- a/AKModel/tests.py +++ b/AKModel/tests/test_views.py @@ -7,8 +7,19 @@ from django.contrib.messages.storage.base import Message from django.test import TestCase from django.urls import reverse_lazy, reverse -from AKModel.models import Event, AKOwner, AKCategory, AKTrack, AKRequirement, AK, Room, AKSlot, AKOrgaMessage, \ - ConstraintViolation, DefaultSlot +from AKModel.models import ( + Event, + AKOwner, + AKCategory, + AKTrack, + AKRequirement, + AK, + Room, + AKSlot, + AKOrgaMessage, + ConstraintViolation, + DefaultSlot, +) class BasicViewTests: @@ -29,9 +40,10 @@ class BasicViewTests: since the test framework does not understand the concept of abstract test definitions and would handle this class as real test case otherwise, distorting the test results. """ + # pylint: disable=no-member VIEWS = [] - APP_NAME = '' + APP_NAME = "" VIEWS_STAFF_ONLY = [] EDIT_TESTCASES = [] @@ -41,16 +53,26 @@ class BasicViewTests: """ user_model = get_user_model() self.staff_user = user_model.objects.create( - username='Test Staff User', email='teststaff@example.com', password='staffpw', - is_staff=True, is_active=True + username="Test Staff User", + email="teststaff@example.com", + password="staffpw", + is_staff=True, + is_active=True, ) self.admin_user = user_model.objects.create( - username='Test Admin User', email='testadmin@example.com', password='adminpw', - is_staff=True, is_superuser=True, is_active=True + username="Test Admin User", + email="testadmin@example.com", + password="adminpw", + is_staff=True, + is_superuser=True, + is_active=True, ) self.deactivated_user = user_model.objects.create( - username='Test Deactivated User', email='testdeactivated@example.com', password='deactivatedpw', - is_staff=True, is_active=False + username="Test Deactivated User", + email="testdeactivated@example.com", + password="deactivatedpw", + is_staff=True, + is_active=False, ) def _name_and_url(self, view_name): @@ -62,7 +84,9 @@ class BasicViewTests: :return: full view name with prefix if applicable, url of the view :rtype: str, str """ - view_name_with_prefix = f"{self.APP_NAME}:{view_name[0]}" if self.APP_NAME != "" else view_name[0] + view_name_with_prefix = ( + f"{self.APP_NAME}:{view_name[0]}" if self.APP_NAME != "" else view_name[0] + ) url = reverse(view_name_with_prefix, kwargs=view_name[1]) return view_name_with_prefix, url @@ -74,7 +98,7 @@ class BasicViewTests: :param expected_message: message that should be shown :param msg_prefix: prefix for the error message when test fails """ - messages:List[Message] = list(get_messages(response.wsgi_request)) + messages: List[Message] = list(get_messages(response.wsgi_request)) msg_count = "No message shown to user" msg_content = "Wrong message, expected '{expected_message}'" @@ -95,10 +119,16 @@ class BasicViewTests: view_name_with_prefix, url = self._name_and_url(view_name) try: response = self.client.get(url) - self.assertEqual(response.status_code, 200, msg=f"{view_name_with_prefix} ({url}) broken") - except Exception: # pylint: disable=broad-exception-caught - self.fail(f"An error occurred during rendering of {view_name_with_prefix} ({url}):" - f"\n\n{traceback.format_exc()}") + self.assertEqual( + response.status_code, + 200, + msg=f"{view_name_with_prefix} ({url}) broken", + ) + except Exception: # pylint: disable=broad-exception-caught + self.fail( + f"An error occurred during rendering of {view_name_with_prefix} ({url}):" + f"\n\n{traceback.format_exc()}" + ) def test_access_control_staff_only(self): """ @@ -107,11 +137,16 @@ class BasicViewTests: # Not logged in? Views should not be visible self.client.logout() for view_name_info in self.VIEWS_STAFF_ONLY: - expected_response_code = 302 if len(view_name_info) == 2 else view_name_info[2] + expected_response_code = ( + 302 if len(view_name_info) == 2 else view_name_info[2] + ) view_name_with_prefix, url = self._name_and_url(view_name_info) response = self.client.get(url) - self.assertEqual(response.status_code, expected_response_code, - msg=f"{view_name_with_prefix} ({url}) accessible by non-staff") + self.assertEqual( + response.status_code, + expected_response_code, + msg=f"{view_name_with_prefix} ({url}) accessible by non-staff", + ) # Logged in? Views should be visible self.client.force_login(self.staff_user) @@ -119,20 +154,30 @@ class BasicViewTests: view_name_with_prefix, url = self._name_and_url(view_name_info) try: response = self.client.get(url) - self.assertEqual(response.status_code, 200, - msg=f"{view_name_with_prefix} ({url}) should be accessible for staff (but isn't)") + self.assertEqual( + response.status_code, + 200, + msg=f"{view_name_with_prefix} ({url}) should be accessible for staff (but isn't)", + ) except Exception: # pylint: disable=broad-exception-caught - self.fail(f"An error occurred during rendering of {view_name_with_prefix} ({url}):" - f"\n\n{traceback.format_exc()}") + self.fail( + f"An error occurred during rendering of {view_name_with_prefix} ({url}):" + f"\n\n{traceback.format_exc()}" + ) # Disabled user? Views should not be visible self.client.force_login(self.deactivated_user) for view_name_info in self.VIEWS_STAFF_ONLY: - expected_response_code = 302 if len(view_name_info) == 2 else view_name_info[2] + expected_response_code = ( + 302 if len(view_name_info) == 2 else view_name_info[2] + ) view_name_with_prefix, url = self._name_and_url(view_name_info) response = self.client.get(url) - self.assertEqual(response.status_code, expected_response_code, - msg=f"{view_name_with_prefix} ({url}) still accessible for deactivated user") + self.assertEqual( + response.status_code, + expected_response_code, + msg=f"{view_name_with_prefix} ({url}) still accessible for deactivated user", + ) def _to_sendable_value(self, val): """ @@ -182,16 +227,26 @@ class BasicViewTests: self.client.logout() response = self.client.get(url) - self.assertEqual(response.status_code, 200, msg=f"{name}: Could not load edit form via GET ({url})") + self.assertEqual( + response.status_code, + 200, + msg=f"{name}: Could not load edit form via GET ({url})", + ) form = response.context[form_name] - data = {k:self._to_sendable_value(v) for k,v in form.initial.items()} + data = {k: self._to_sendable_value(v) for k, v in form.initial.items()} response = self.client.post(url, data=data) if expected_code == 200: - self.assertEqual(response.status_code, 200, msg=f"{name}: Did not return 200 ({url}") + self.assertEqual( + response.status_code, 200, msg=f"{name}: Did not return 200 ({url}" + ) elif expected_code == 302: - self.assertRedirects(response, target_url, msg_prefix=f"{name}: Did not redirect ({url} -> {target_url}") + self.assertRedirects( + response, + target_url, + msg_prefix=f"{name}: Did not redirect ({url} -> {target_url}", + ) if expected_message != "": self._assert_message(response, expected_message, msg_prefix=f"{name}") @@ -200,32 +255,44 @@ class ModelViewTests(BasicViewTests, TestCase): """ Basic view test cases for views from AKModel plus some custom tests """ - fixtures = ['model.json'] + + fixtures = ["model.json"] ADMIN_MODELS = [ - (Event, 'event'), (AKOwner, 'akowner'), (AKCategory, 'akcategory'), (AKTrack, 'aktrack'), - (AKRequirement, 'akrequirement'), (AK, 'ak'), (Room, 'room'), (AKSlot, 'akslot'), - (AKOrgaMessage, 'akorgamessage'), (ConstraintViolation, 'constraintviolation'), - (DefaultSlot, 'defaultslot') + (Event, "event"), + (AKOwner, "akowner"), + (AKCategory, "akcategory"), + (AKTrack, "aktrack"), + (AKRequirement, "akrequirement"), + (AK, "ak"), + (Room, "room"), + (AKSlot, "akslot"), + (AKOrgaMessage, "akorgamessage"), + (ConstraintViolation, "constraintviolation"), + (DefaultSlot, "defaultslot"), ] VIEWS_STAFF_ONLY = [ - ('admin:index', {}), - ('admin:event_status', {'event_slug': 'kif42'}), - ('admin:event_requirement_overview', {'event_slug': 'kif42'}), - ('admin:ak_csv_export', {'event_slug': 'kif42'}), - ('admin:ak_json_export', {'event_slug': 'kif42'}), - ('admin:ak_wiki_export', {'slug': 'kif42'}), - ('admin:ak_schedule_json_import', {'event_slug': 'kif42'}), - ('admin:ak_delete_orga_messages', {'event_slug': 'kif42'}), - ('admin:ak_slide_export', {'event_slug': 'kif42'}), - ('admin:default-slots-editor', {'event_slug': 'kif42'}), - ('admin:room-import', {'event_slug': 'kif42'}), - ('admin:new_event_wizard_start', {}), + ("admin:index", {}), + ("admin:event_status", {"event_slug": "kif42"}), + ("admin:event_requirement_overview", {"event_slug": "kif42"}), + ("admin:ak_csv_export", {"event_slug": "kif42"}), + ("admin:ak_json_export", {"event_slug": "kif42"}), + ("admin:ak_wiki_export", {"slug": "kif42"}), + ("admin:ak_schedule_json_import", {"event_slug": "kif42"}), + ("admin:ak_delete_orga_messages", {"event_slug": "kif42"}), + ("admin:ak_slide_export", {"event_slug": "kif42"}), + ("admin:default-slots-editor", {"event_slug": "kif42"}), + ("admin:room-import", {"event_slug": "kif42"}), + ("admin:new_event_wizard_start", {}), ] EDIT_TESTCASES = [ - {'view': 'admin:default-slots-editor', 'kwargs': {'event_slug': 'kif42'}, "admin": True}, + { + "view": "admin:default-slots-editor", + "kwargs": {"event_slug": "kif42"}, + "admin": True, + }, ] def test_admin(self): @@ -236,24 +303,32 @@ class ModelViewTests(BasicViewTests, TestCase): for model in self.ADMIN_MODELS: # Special treatment for a subset of views (where we exchanged default functionality, e.g., create views) if model[1] == "event": - _, url = self._name_and_url(('admin:new_event_wizard_start', {})) + _, url = self._name_and_url(("admin:new_event_wizard_start", {})) elif model[1] == "room": - _, url = self._name_and_url(('admin:room-new', {})) + _, url = self._name_and_url(("admin:room-new", {})) # Otherwise, just call the creation form view else: - _, url = self._name_and_url((f'admin:AKModel_{model[1]}_add', {})) + _, url = self._name_and_url((f"admin:AKModel_{model[1]}_add", {})) response = self.client.get(url) - self.assertEqual(response.status_code, 200, msg=f"Add form for model {model[1]} ({url}) broken") + self.assertEqual( + response.status_code, + 200, + msg=f"Add form for model {model[1]} ({url}) broken", + ) for model in self.ADMIN_MODELS: # Test the update view using the first existing instance of each model m = model[0].objects.first() if m is not None: _, url = self._name_and_url( - (f'admin:AKModel_{model[1]}_change', {'object_id': m.pk}) + (f"admin:AKModel_{model[1]}_change", {"object_id": m.pk}) ) response = self.client.get(url) - self.assertEqual(response.status_code, 200, msg=f"Edit form for model {model[1]} ({url}) broken") + self.assertEqual( + response.status_code, + 200, + msg=f"Edit form for model {model[1]} ({url}) broken", + ) def test_wiki_export(self): """ @@ -262,17 +337,27 @@ class ModelViewTests(BasicViewTests, TestCase): """ self.client.force_login(self.admin_user) - export_url = reverse_lazy("admin:ak_wiki_export", kwargs={'slug': 'kif42'}) + export_url = reverse_lazy("admin:ak_wiki_export", kwargs={"slug": "kif42"}) response = self.client.get(export_url) self.assertEqual(response.status_code, 200, "Export not working at all") export_count = 0 for _, aks in response.context["categories_with_aks"]: for ak in aks: - self.assertEqual(ak.include_in_export, True, - f"AK with export flag set to False (pk={ak.pk}) included in export") - self.assertNotEqual(ak.pk, 1, "AK known to be excluded from export (PK 1) included in export") + self.assertEqual( + ak.include_in_export, + True, + f"AK with export flag set to False (pk={ak.pk}) included in export", + ) + self.assertNotEqual( + ak.pk, + 1, + "AK known to be excluded from export (PK 1) included in export", + ) export_count += 1 - self.assertEqual(export_count, AK.objects.filter(event_id=2, include_in_export=True).count(), - "Wiki export contained the wrong number of AKs") + self.assertEqual( + export_count, + AK.objects.filter(event_id=2, include_in_export=True).count(), + "Wiki export contained the wrong number of AKs", + ) diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index 5e642dd5..bca1c5b3 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -50,11 +50,6 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): context_object_name = "slots" title = _("AK JSON Export") - - def _test_slot_contained(self, slot: Availability, availabilities: List[Availability]) -> bool: - """Test if slot is contained in any member of availabilities.""" - return any(availability.contains(slot) for availability in availabilities) - def _test_event_not_covered(self, availabilities: List[Availability]) -> bool: """Test if event is not covered by availabilities.""" return not Availability.is_event_covered(self.event, availabilities) @@ -70,8 +65,7 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): def _test_add_constraint(self, slot: Availability, availabilities: List[Availability]) -> bool: """Test if object is not available for whole event and may happen during slot.""" return ( - self._test_event_not_covered(availabilities) - and self._test_slot_contained(slot, availabilities) + self._test_event_not_covered(availabilities) and slot.is_covered(availabilities) ) def _generate_time_constraints( @@ -161,7 +155,8 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): current_block.append({ "id": str(timeslot.idx), "info": { - "start": timeslot.avail.simplified, + "start": timeslot.avail.start.astimezone(self.event.timezone).strftime("%Y-%m-%d %H:%M"), + "end": timeslot.avail.end.astimezone(self.event.timezone).strftime("%Y-%m-%d %H:%M"), }, "fulfilled_time_constraints": time_constraints, }) diff --git a/AKPlan/tests.py b/AKPlan/tests.py index 69365c2b..3f00061a 100644 --- a/AKPlan/tests.py +++ b/AKPlan/tests.py @@ -1,6 +1,6 @@ from django.test import TestCase -from AKModel.tests import BasicViewTests +from AKModel.tests.test_views import BasicViewTests class PlanViewTests(BasicViewTests, TestCase): diff --git a/AKScheduling/api.py b/AKScheduling/api.py index e78fda78..cfd476ec 100644 --- a/AKScheduling/api.py +++ b/AKScheduling/api.py @@ -55,7 +55,9 @@ class EventsView(LoginRequiredMixin, EventSlugMixin, ListView): model = AKSlot def get_queryset(self): - return super().get_queryset().select_related('ak').filter(event=self.event, room__isnull=False) + return super().get_queryset().select_related('ak').filter( + event=self.event, room__isnull=False, start__isnull=False + ) def render_to_response(self, context, **response_kwargs): return JsonResponse( diff --git a/AKScheduling/models.py b/AKScheduling/models.py index aa6be3d0..8f34c151 100644 --- a/AKScheduling/models.py +++ b/AKScheduling/models.py @@ -365,8 +365,8 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs): new_violations = [] # For all slots in this room... - if instance.room: - for other_slot in instance.room.akslot_set.all(): + if instance.room and instance.start: + for other_slot in instance.room.akslot_set.filter(start__isnull=False): if other_slot != instance: # ... find overlapping slots... if instance.overlaps(other_slot): diff --git a/AKScheduling/tests.py b/AKScheduling/tests.py index 0996eedd..44f25719 100644 --- a/AKScheduling/tests.py +++ b/AKScheduling/tests.py @@ -4,7 +4,7 @@ from datetime import timedelta from django.test import TestCase from django.utils import timezone -from AKModel.tests import BasicViewTests +from AKModel.tests.test_views import BasicViewTests from AKModel.models import AKSlot, Event, Room class ModelViewTests(BasicViewTests, TestCase): diff --git a/AKSubmission/tests.py b/AKSubmission/tests.py index 018289aa..423a9776 100644 --- a/AKSubmission/tests.py +++ b/AKSubmission/tests.py @@ -5,7 +5,7 @@ from django.urls import reverse_lazy from django.utils.datetime_safe import datetime from AKModel.models import AK, AKSlot, Event -from AKModel.tests import BasicViewTests +from AKModel.tests.test_views import BasicViewTests class ModelViewTests(BasicViewTests, TestCase): -- GitLab From ab8a00149c2591c231d41ce600908f684846b4b7 Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Thu, 6 Feb 2025 16:51:16 +0100 Subject: [PATCH 58/77] Update locale messages --- AKModel/locale/de_DE/LC_MESSAGES/django.po | 155 +++++++++++---------- 1 file changed, 78 insertions(+), 77 deletions(-) diff --git a/AKModel/locale/de_DE/LC_MESSAGES/django.po b/AKModel/locale/de_DE/LC_MESSAGES/django.po index 42b38565..88eb85d5 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-01-22 19:00+0000\n" +"POT-Creation-Date: 2025-02-06 15:48+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:168 AKModel/models.py:689 AKModel/models.py:1114 -#: AKModel/models.py:1150 +#: AKModel/admin.py:168 AKModel/models.py:689 AKModel/models.py:1120 +#: AKModel/models.py:1156 #: AKModel/templates/admin/AKModel/status/event_aks.html:10 #: AKModel/views/manage.py:75 AKModel/views/status.py:97 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:320 AKModel/views/ak.py:237 +#: AKModel/admin.py:320 AKModel/views/ak.py:241 msgid "Reset interest in AKs" msgstr "Interesse an AKs zurücksetzen" -#: AKModel/admin.py:330 AKModel/views/ak.py:252 +#: AKModel/admin.py:330 AKModel/views/ak.py:256 msgid "Reset AKs' interest counters" msgstr "Interessenszähler der AKs zurücksetzen" @@ -84,7 +84,7 @@ msgstr "Constraintverletzungen auf Level \"Violation\" setzen" msgid "Set Constraint Violations to level \"warning\"" msgstr "Constraintverletzungen auf Level \"Warning\" setzen" -#: AKModel/availability/forms.py:25 AKModel/availability/models.py:299 +#: AKModel/availability/forms.py:25 AKModel/availability/models.py:308 msgid "Availability" msgstr "Verfügbarkeit" @@ -112,15 +112,15 @@ msgstr "Bitte Verfügbarkeiten eintragen!" #: AKModel/availability/models.py:43 AKModel/models.py:160 #: AKModel/models.py:489 AKModel/models.py:566 AKModel/models.py:599 #: AKModel/models.py:625 AKModel/models.py:679 AKModel/models.py:821 -#: AKModel/models.py:897 AKModel/models.py:1053 AKModel/models.py:1110 -#: AKModel/models.py:1301 +#: AKModel/models.py:897 AKModel/models.py:1059 AKModel/models.py:1116 +#: AKModel/models.py:1307 msgid "Event" msgstr "Event" #: AKModel/availability/models.py:44 AKModel/models.py:490 #: AKModel/models.py:567 AKModel/models.py:600 AKModel/models.py:626 #: AKModel/models.py:680 AKModel/models.py:822 AKModel/models.py:898 -#: AKModel/models.py:1054 AKModel/models.py:1111 AKModel/models.py:1302 +#: AKModel/models.py:1060 AKModel/models.py:1117 AKModel/models.py:1308 msgid "Associated event" msgstr "Zugehöriges Event" @@ -133,7 +133,7 @@ msgid "Person whose availability this is" msgstr "Person deren Verfügbarkeit hier abgebildet wird" #: AKModel/availability/models.py:61 AKModel/models.py:825 -#: AKModel/models.py:887 AKModel/models.py:1120 +#: AKModel/models.py:887 AKModel/models.py:1126 msgid "Room" msgstr "Raum" @@ -142,7 +142,7 @@ msgid "Room whose availability this is" msgstr "Raum dessen Verfügbarkeit hier abgebildet wird" #: AKModel/availability/models.py:70 AKModel/models.py:688 -#: AKModel/models.py:886 AKModel/models.py:1048 +#: AKModel/models.py:886 AKModel/models.py:1054 msgid "AK" msgstr "AK" @@ -151,7 +151,7 @@ msgid "AK whose availability this is" msgstr "Verfügbarkeiten" #: AKModel/availability/models.py:79 AKModel/models.py:570 -#: AKModel/models.py:1126 +#: AKModel/models.py:1132 msgid "AK Category" msgstr "AK-Kategorie" @@ -159,7 +159,7 @@ msgstr "AK-Kategorie" msgid "AK Category whose availability this is" msgstr "AK-Kategorie, deren Verfügbarkeit hier abgebildet wird" -#: AKModel/availability/models.py:300 AKModel/models.py:721 +#: AKModel/availability/models.py:309 AKModel/models.py:721 msgid "Availabilities" msgstr "Verfügbarkeiten" @@ -221,7 +221,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:1295 +#: AKModel/forms.py:189 AKModel/models.py:1301 msgid "Default Slots" msgstr "Standardslots" @@ -447,8 +447,9 @@ msgid "" "Duration of AK {ak_name} assigned by solver ({solver_duration} hours) is " "less than the duration required by the slot ({slot_duration} hours)" msgstr "" -"Die dem AK {ak_name} vom Solver zugewiesene Dauer ({solver_duration} Stunden) ist " -"kürzer als die aktuell vorgesehene Dauer des Slots ({slot_duration} Stunden)" +"Die dem AK {ak_name} vom Solver zugewiesene Dauer ({solver_duration} " +"Stunden) ist kürzer als die aktuell vorgesehene Dauer des Slots " +"({slot_duration} Stunden)" #: AKModel/models.py:454 #, python-brace-format @@ -456,8 +457,8 @@ msgid "" "Fixed AK {ak_name} assigned by solver to room {solver_room} is fixed to room " "{slot_room}" 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." +"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:465 #, python-brace-format @@ -465,8 +466,8 @@ msgid "" "Fixed AK {ak_name} assigned by solver to start at {solver_start} is fixed to " "start at {slot_start}" 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." +"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:484 msgid "Nickname" @@ -500,7 +501,7 @@ msgstr "Internet Link" msgid "Link to Homepage" msgstr "Link zu Homepage oder Webseite" -#: AKModel/models.py:493 AKModel/models.py:1119 +#: AKModel/models.py:493 AKModel/models.py:1125 msgid "AK Owner" msgstr "AK-Leitung" @@ -560,7 +561,7 @@ msgstr "AK-Tracks" msgid "Name of the Requirement" msgstr "Name der Anforderung" -#: AKModel/models.py:629 AKModel/models.py:1123 +#: AKModel/models.py:629 AKModel/models.py:1129 msgid "AK Requirement" msgstr "AK-Anforderung" @@ -747,11 +748,11 @@ msgstr "AK, der zugeordnet wird" msgid "Room the AK will take place in" msgstr "Raum in dem der AK stattfindet" -#: AKModel/models.py:889 AKModel/models.py:1298 +#: AKModel/models.py:889 AKModel/models.py:1304 msgid "Slot Begin" msgstr "Beginn des Slots" -#: AKModel/models.py:889 AKModel/models.py:1298 +#: AKModel/models.py:889 AKModel/models.py:1304 msgid "Time and date the slot begins" msgstr "Zeit und Datum zu der der AK beginnt" @@ -779,7 +780,7 @@ msgstr "Letzte Aktualisierung" msgid "AK Slot" msgstr "AK-Slot" -#: AKModel/models.py:904 AKModel/models.py:1116 AKModel/models.py:1151 +#: AKModel/models.py:904 AKModel/models.py:1122 AKModel/models.py:1157 msgid "AK Slots" msgstr "AK-Slot" @@ -787,187 +788,187 @@ msgstr "AK-Slot" msgid "Not scheduled yet" msgstr "Noch nicht geplant" -#: AKModel/models.py:1049 +#: AKModel/models.py:1055 msgid "AK this message belongs to" msgstr "AK zu dem die Nachricht gehört" -#: AKModel/models.py:1050 +#: AKModel/models.py:1056 msgid "Message text" msgstr "Nachrichtentext" -#: AKModel/models.py:1051 +#: AKModel/models.py:1057 msgid "Message to the organizers. This is not publicly visible." msgstr "" "Nachricht an die Organisator*innen. Diese ist nicht öffentlich sichtbar." -#: AKModel/models.py:1055 +#: AKModel/models.py:1061 msgid "Resolved" msgstr "Erledigt" -#: AKModel/models.py:1056 +#: AKModel/models.py:1062 msgid "This message has been resolved (no further action needed)" msgstr "" "Diese Nachricht wurde vollständig bearbeitet (keine weiteren Aktionen " "notwendig)" -#: AKModel/models.py:1059 +#: AKModel/models.py:1065 msgid "AK Orga Message" msgstr "AK-Organachricht" -#: AKModel/models.py:1060 +#: AKModel/models.py:1066 msgid "AK Orga Messages" msgstr "AK-Organachrichten" -#: AKModel/models.py:1077 +#: AKModel/models.py:1083 msgid "Constraint Violation" msgstr "Constraintverletzung" -#: AKModel/models.py:1078 +#: AKModel/models.py:1084 msgid "Constraint Violations" msgstr "Constraintverletzungen" -#: AKModel/models.py:1085 +#: AKModel/models.py:1091 msgid "Owner has two parallel slots" msgstr "Leitung hat zwei Slots parallel" -#: AKModel/models.py:1086 +#: AKModel/models.py:1092 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:1087 +#: AKModel/models.py:1093 msgid "Room has two AK slots scheduled at the same time" msgstr "Raum hat zwei AK Slots gleichzeitig" -#: AKModel/models.py:1088 +#: AKModel/models.py:1094 msgid "Room does not satisfy the requirement of the scheduled AK" msgstr "Room erfüllt die Anforderungen des platzierten AKs nicht" -#: AKModel/models.py:1089 +#: AKModel/models.py:1095 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:1090 +#: AKModel/models.py:1096 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:1092 +#: AKModel/models.py:1098 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:1093 +#: AKModel/models.py:1099 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:1094 +#: AKModel/models.py:1100 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:1095 +#: AKModel/models.py:1101 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:1096 +#: AKModel/models.py:1102 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:1102 +#: AKModel/models.py:1108 msgid "Warning" msgstr "Warnung" -#: AKModel/models.py:1103 +#: AKModel/models.py:1109 msgid "Violation" msgstr "Verletzung" -#: AKModel/models.py:1105 +#: AKModel/models.py:1111 msgid "Type" msgstr "Art" -#: AKModel/models.py:1106 +#: AKModel/models.py:1112 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:1107 +#: AKModel/models.py:1113 msgid "Level" msgstr "Level" -#: AKModel/models.py:1108 +#: AKModel/models.py:1114 msgid "Severity level of the violation" msgstr "Schweregrad der Verletzung" -#: AKModel/models.py:1115 +#: AKModel/models.py:1121 msgid "AK(s) belonging to this constraint" msgstr "AK(s), die zu diesem Constraint gehören" -#: AKModel/models.py:1117 +#: AKModel/models.py:1123 msgid "AK Slot(s) belonging to this constraint" msgstr "AK Slot(s), die zu diesem Constraint gehören" -#: AKModel/models.py:1119 +#: AKModel/models.py:1125 msgid "AK Owner belonging to this constraint" msgstr "AK Leitung(en), die zu diesem Constraint gehören" -#: AKModel/models.py:1121 +#: AKModel/models.py:1127 msgid "Room belonging to this constraint" msgstr "Raum, der zu diesem Constraint gehört" -#: AKModel/models.py:1124 +#: AKModel/models.py:1130 msgid "AK Requirement belonging to this constraint" msgstr "AK Anforderung, die zu diesem Constraint gehört" -#: AKModel/models.py:1126 +#: AKModel/models.py:1132 msgid "AK Category belonging to this constraint" msgstr "AK Kategorie, di zu diesem Constraint gehört" -#: AKModel/models.py:1128 +#: AKModel/models.py:1134 msgid "Comment" msgstr "Kommentar" -#: AKModel/models.py:1128 +#: AKModel/models.py:1134 msgid "Comment or further details for this violation" msgstr "Kommentar oder weitere Details zu dieser Vereletzung" -#: AKModel/models.py:1131 +#: AKModel/models.py:1137 msgid "Timestamp" msgstr "Timestamp" -#: AKModel/models.py:1131 +#: AKModel/models.py:1137 msgid "Time of creation" msgstr "Zeitpunkt der ERstellung" -#: AKModel/models.py:1132 +#: AKModel/models.py:1138 msgid "Manually Resolved" msgstr "Manuell behoben" -#: AKModel/models.py:1133 +#: AKModel/models.py:1139 msgid "Mark this violation manually as resolved" msgstr "Markiere diese Verletzung manuell als behoben" -#: AKModel/models.py:1160 +#: AKModel/models.py:1166 #: AKModel/templates/admin/AKModel/requirements_overview.html:27 msgid "Details" msgstr "Details" -#: AKModel/models.py:1294 +#: AKModel/models.py:1300 msgid "Default Slot" msgstr "Standardslot" -#: AKModel/models.py:1299 +#: AKModel/models.py:1305 msgid "Slot End" msgstr "Ende des Slots" -#: AKModel/models.py:1299 +#: AKModel/models.py:1305 msgid "Time and date the slot ends" msgstr "Zeit und Datum zu der der Slot endet" -#: AKModel/models.py:1304 +#: AKModel/models.py:1310 msgid "Primary categories" msgstr "Primäre Kategorien" -#: AKModel/models.py:1305 +#: AKModel/models.py:1311 msgid "Categories that should be assigned to this slot primarily" msgstr "Kategorieren, die diesem Slot primär zugewiesen werden sollen" @@ -1177,35 +1178,35 @@ msgstr "AK-CSV-Export" msgid "AK JSON Export" msgstr "AK-JSON-Export" -#: AKModel/views/ak.py:186 +#: AKModel/views/ak.py:190 msgid "AK Wiki Export" msgstr "AK-Wiki-Export" -#: AKModel/views/ak.py:197 AKModel/views/manage.py:55 +#: AKModel/views/ak.py:201 AKModel/views/manage.py:55 msgid "Wishes" msgstr "Wünsche" -#: AKModel/views/ak.py:209 +#: AKModel/views/ak.py:213 msgid "Delete AK Orga Messages" msgstr "AK-Organachrichten löschen" -#: AKModel/views/ak.py:227 +#: AKModel/views/ak.py:231 msgid "AK Orga Messages successfully deleted" msgstr "AK-Organachrichten erfolgreich gelöscht" -#: AKModel/views/ak.py:239 +#: AKModel/views/ak.py:243 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:240 +#: AKModel/views/ak.py:244 msgid "Reset of interest in AKs successful." msgstr "Interesse an AKs erfolgreich zurückgesetzt." -#: AKModel/views/ak.py:254 +#: AKModel/views/ak.py:258 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:255 +#: AKModel/views/ak.py:259 msgid "AKs' interest counters set back to 0." msgstr "Interessenszähler der AKs zurückgesetzt" -- GitLab From 74ac670a74e517abed97b72e285280486b97ecce Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Thu, 6 Feb 2025 17:41:16 +0100 Subject: [PATCH 59/77] Add export slot length to event --- AKModel/migrations/0061_event_export_slot.py | 24 ++++++++++++++++++++ AKModel/models.py | 18 +++++++++++---- AKModel/views/ak.py | 10 ++------ 3 files changed, 39 insertions(+), 13 deletions(-) create mode 100644 AKModel/migrations/0061_event_export_slot.py diff --git a/AKModel/migrations/0061_event_export_slot.py b/AKModel/migrations/0061_event_export_slot.py new file mode 100644 index 00000000..3b40b88c --- /dev/null +++ b/AKModel/migrations/0061_event_export_slot.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.13 on 2025-02-06 16:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("AKModel", "0060_orga_message_resolved"), + ] + + operations = [ + migrations.AddField( + model_name="event", + name="export_slot", + field=models.DecimalField( + decimal_places=2, + default=1, + help_text="Slot duration in hours that is used in the timeslot discretization, when this event is exported for the solver.", + max_digits=4, + verbose_name="Export Slot Length", + ), + ), + ] diff --git a/AKModel/models.py b/AKModel/models.py index 1cd3f1ba..b3820a15 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -151,6 +151,12 @@ class Event(models.Model): 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.' + )) + contact_email = models.EmailField(verbose_name=_("Contact email address"), blank=True, help_text=_("An email address that is displayed on every page " @@ -336,7 +342,7 @@ class Event(models.Model): return slot_index - def uniform_time_slots(self, *, slots_in_an_hour: float = 1.0) -> Iterable[TimeslotBlock]: + def uniform_time_slots(self, *, slots_in_an_hour: float) -> Iterable[TimeslotBlock]: """Uniformly discretize the entire event into blocks of timeslots. Discretizes entire event uniformly. May not necessarily result in a single block @@ -358,7 +364,7 @@ class Event(models.Model): constraints=all_category_constraints, ) - def default_time_slots(self, *, slots_in_an_hour: float = 1.0) -> Iterable[TimeslotBlock]: + def default_time_slots(self, *, slots_in_an_hour: float) -> Iterable[TimeslotBlock]: """Discretize all default slots into blocks of timeslots. In the discretization each default slot corresponds to one block. @@ -384,7 +390,7 @@ class Event(models.Model): constraints=category_constraints, ) - def discretize_timeslots(self, *, slots_in_an_hour: float = 1.0) -> Iterable[TimeslotBlock]: + def discretize_timeslots(self, *, slots_in_an_hour: float | None = None) -> Iterable[TimeslotBlock]: """"Choose discretization scheme. Uses default_time_slots if the event has any DefaultSlot, otherwise uniform_time_slots. @@ -395,6 +401,9 @@ class Event(models.Model): :ytype: list of TimeslotBlock """ + if slots_in_an_hour is None: + slots_in_an_hour = float(self.export_slot) + if DefaultSlot.objects.filter(event=self).exists(): # discretize default slots if they exists yield from merge_blocks(self.default_time_slots(slots_in_an_hour=slots_in_an_hour)) @@ -1007,10 +1016,9 @@ class AKSlot(models.Model): ceil_offet_eps = decimal.Decimal(1e-4) - # self.slots_in_an_hour is set in AKJSONExportView data = { "id": str(self.pk), - "duration": math.ceil(self.duration * self.slots_in_an_hour - ceil_offet_eps), + "duration": math.ceil(self.duration * self.event.export_slot - ceil_offet_eps), "properties": { "conflicts": [str(conflict.pk) for conflict in conflict_slots.all()] diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index bca1c5b3..d9a5e6a8 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -91,17 +91,11 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): 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), }, + "info": {"duration": (1.0 / float(self.event.export_slot)), }, "blocks": [], } - for slot in context["slots"]: - slot.slots_in_an_hour = SLOTS_IN_AN_HOUR - ak_availabilities = { ak.pk: Availability.union(ak.availabilities.all()) for ak in AK.objects.filter(event=self.event).all() @@ -115,7 +109,7 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): for person in AKOwner.objects.filter(event=self.event) } - blocks = self.event.discretize_timeslots(slots_in_an_hour=SLOTS_IN_AN_HOUR) + blocks = self.event.discretize_timeslots() for block in blocks: current_block = [] -- GitLab From 599aec3562b2ff67d8ab183e33beea8225b60fcf Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Thu, 6 Feb 2025 17:41:45 +0100 Subject: [PATCH 60/77] Update language files --- AKModel/locale/de_DE/LC_MESSAGES/django.po | 332 +++++++++++---------- 1 file changed, 172 insertions(+), 160 deletions(-) diff --git a/AKModel/locale/de_DE/LC_MESSAGES/django.po b/AKModel/locale/de_DE/LC_MESSAGES/django.po index 88eb85d5..d4f3db1b 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-02-06 15:48+0000\n" +"POT-Creation-Date: 2025-02-06 16:38+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:168 AKModel/models.py:689 AKModel/models.py:1120 -#: AKModel/models.py:1156 +#: AKModel/admin.py:168 AKModel/models.py:698 AKModel/models.py:1128 +#: AKModel/models.py:1164 #: AKModel/templates/admin/AKModel/status/event_aks.html:10 #: AKModel/views/manage.py:75 AKModel/views/status.py:97 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:320 AKModel/views/ak.py:241 +#: AKModel/admin.py:320 AKModel/views/ak.py:235 msgid "Reset interest in AKs" msgstr "Interesse an AKs zurücksetzen" -#: AKModel/admin.py:330 AKModel/views/ak.py:256 +#: AKModel/admin.py:330 AKModel/views/ak.py:250 msgid "Reset AKs' interest counters" msgstr "Interessenszähler der AKs zurücksetzen" @@ -109,18 +109,18 @@ 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:160 -#: AKModel/models.py:489 AKModel/models.py:566 AKModel/models.py:599 -#: AKModel/models.py:625 AKModel/models.py:679 AKModel/models.py:821 -#: AKModel/models.py:897 AKModel/models.py:1059 AKModel/models.py:1116 -#: AKModel/models.py:1307 +#: AKModel/availability/models.py:43 AKModel/models.py:166 +#: AKModel/models.py:498 AKModel/models.py:575 AKModel/models.py:608 +#: AKModel/models.py:634 AKModel/models.py:688 AKModel/models.py:830 +#: AKModel/models.py:906 AKModel/models.py:1067 AKModel/models.py:1124 +#: AKModel/models.py:1315 msgid "Event" msgstr "Event" -#: AKModel/availability/models.py:44 AKModel/models.py:490 -#: AKModel/models.py:567 AKModel/models.py:600 AKModel/models.py:626 -#: AKModel/models.py:680 AKModel/models.py:822 AKModel/models.py:898 -#: AKModel/models.py:1060 AKModel/models.py:1117 AKModel/models.py:1308 +#: AKModel/availability/models.py:44 AKModel/models.py:499 +#: AKModel/models.py:576 AKModel/models.py:609 AKModel/models.py:635 +#: AKModel/models.py:689 AKModel/models.py:831 AKModel/models.py:907 +#: AKModel/models.py:1068 AKModel/models.py:1125 AKModel/models.py:1316 msgid "Associated event" msgstr "Zugehöriges Event" @@ -132,8 +132,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:825 -#: AKModel/models.py:887 AKModel/models.py:1126 +#: AKModel/availability/models.py:61 AKModel/models.py:834 +#: AKModel/models.py:896 AKModel/models.py:1134 msgid "Room" msgstr "Raum" @@ -141,8 +141,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:688 -#: AKModel/models.py:886 AKModel/models.py:1054 +#: AKModel/availability/models.py:70 AKModel/models.py:697 +#: AKModel/models.py:895 AKModel/models.py:1062 msgid "AK" msgstr "AK" @@ -150,8 +150,8 @@ msgstr "AK" msgid "AK whose availability this is" msgstr "Verfügbarkeiten" -#: AKModel/availability/models.py:79 AKModel/models.py:570 -#: AKModel/models.py:1132 +#: AKModel/availability/models.py:79 AKModel/models.py:579 +#: AKModel/models.py:1140 msgid "AK Category" msgstr "AK-Kategorie" @@ -159,7 +159,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:721 +#: AKModel/availability/models.py:309 AKModel/models.py:730 msgid "Availabilities" msgstr "Verfügbarkeiten" @@ -221,7 +221,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:1301 +#: AKModel/forms.py:189 AKModel/models.py:1309 msgid "Default Slots" msgstr "Standardslots" @@ -293,8 +293,8 @@ msgstr "Aktivieren?" msgid "Finish" msgstr "Abschluss" -#: AKModel/models.py:120 AKModel/models.py:558 AKModel/models.py:596 -#: AKModel/models.py:623 AKModel/models.py:641 AKModel/models.py:813 +#: AKModel/models.py:120 AKModel/models.py:567 AKModel/models.py:605 +#: AKModel/models.py:632 AKModel/models.py:650 AKModel/models.py:822 msgid "Name" msgstr "Name" @@ -420,11 +420,23 @@ msgstr "Standardslotlänge" 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:155 +#: AKModel/models.py:154 +msgid "Export Slot Length" +msgstr "Export-Slotlänge" + +#: AKModel/models.py:156 +msgid "" +"Slot duration in hours that is used in the timeslot discretization, when " +"this event is exported for the solver." +msgstr "" +"Länge von Slots (in Stunden) in der Zeitslot-Diskretisierung beim " +"JSON-Export dieses Events." + +#: AKModel/models.py:161 msgid "Contact email address" msgstr "E-Mail Kontaktadresse" -#: AKModel/models.py:156 +#: AKModel/models.py:162 msgid "" "An email address that is displayed on every page and can be used for all " "kinds of questions" @@ -432,16 +444,16 @@ msgstr "" "Eine Mailadresse die auf jeder Seite angezeigt wird und für alle Arten von " "Fragen genutzt werden kann" -#: AKModel/models.py:161 +#: AKModel/models.py:167 msgid "Events" msgstr "Events" -#: AKModel/models.py:430 +#: AKModel/models.py:439 #, 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:440 +#: AKModel/models.py:449 #, python-brace-format msgid "" "Duration of AK {ak_name} assigned by solver ({solver_duration} hours) is " @@ -451,7 +463,7 @@ msgstr "" "Stunden) ist kürzer als die aktuell vorgesehene Dauer des Slots " "({slot_duration} Stunden)" -#: AKModel/models.py:454 +#: AKModel/models.py:463 #, python-brace-format msgid "" "Fixed AK {ak_name} assigned by solver to room {solver_room} is fixed to room " @@ -460,7 +472,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:465 +#: AKModel/models.py:474 #, python-brace-format msgid "" "Fixed AK {ak_name} assigned by solver to start at {solver_start} is fixed to " @@ -469,71 +481,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:484 +#: AKModel/models.py:493 msgid "Nickname" msgstr "Spitzname" -#: AKModel/models.py:484 +#: AKModel/models.py:493 msgid "Name to identify an AK owner by" msgstr "Name, durch den eine AK-Leitung identifiziert wird" -#: AKModel/models.py:485 +#: AKModel/models.py:494 msgid "Slug" msgstr "Slug" -#: AKModel/models.py:485 +#: AKModel/models.py:494 msgid "Slug for URL generation" msgstr "Slug für URL-Generierung" -#: AKModel/models.py:486 +#: AKModel/models.py:495 msgid "Institution" msgstr "Instutution" -#: AKModel/models.py:486 +#: AKModel/models.py:495 msgid "Uni etc." msgstr "Universität o.ä." -#: AKModel/models.py:487 AKModel/models.py:650 +#: AKModel/models.py:496 AKModel/models.py:659 msgid "Web Link" msgstr "Internet Link" -#: AKModel/models.py:487 +#: AKModel/models.py:496 msgid "Link to Homepage" msgstr "Link zu Homepage oder Webseite" -#: AKModel/models.py:493 AKModel/models.py:1125 +#: AKModel/models.py:502 AKModel/models.py:1133 msgid "AK Owner" msgstr "AK-Leitung" -#: AKModel/models.py:494 +#: AKModel/models.py:503 msgid "AK Owners" msgstr "AK-Leitungen" -#: AKModel/models.py:558 +#: AKModel/models.py:567 msgid "Name of the AK Category" msgstr "Name der AK-Kategorie" -#: AKModel/models.py:559 AKModel/models.py:597 +#: AKModel/models.py:568 AKModel/models.py:606 msgid "Color" msgstr "Farbe" -#: AKModel/models.py:559 AKModel/models.py:597 +#: AKModel/models.py:568 AKModel/models.py:606 msgid "Color for displaying" msgstr "Farbe für die Anzeige" -#: AKModel/models.py:560 AKModel/models.py:644 +#: AKModel/models.py:569 AKModel/models.py:653 msgid "Description" msgstr "Beschreibung" -#: AKModel/models.py:561 +#: AKModel/models.py:570 msgid "Short description of this AK Category" msgstr "Beschreibung der AK-Kategorie" -#: AKModel/models.py:562 +#: AKModel/models.py:571 msgid "Present by default" msgstr "Defaultmäßig präsentieren" -#: AKModel/models.py:563 +#: AKModel/models.py:572 msgid "" "Present AKs of this category by default if AK owner did not specify whether " "this AK should be presented?" @@ -541,132 +553,132 @@ msgstr "" "AKs dieser Kategorie standardmäßig vorstellen, wenn die Leitungen das für " "ihren AK nicht explizit spezifiziert haben?" -#: AKModel/models.py:571 +#: AKModel/models.py:580 msgid "AK Categories" msgstr "AK-Kategorien" -#: AKModel/models.py:596 +#: AKModel/models.py:605 msgid "Name of the AK Track" msgstr "Name des AK-Tracks" -#: AKModel/models.py:603 +#: AKModel/models.py:612 msgid "AK Track" msgstr "AK-Track" -#: AKModel/models.py:604 +#: AKModel/models.py:613 msgid "AK Tracks" msgstr "AK-Tracks" -#: AKModel/models.py:623 +#: AKModel/models.py:632 msgid "Name of the Requirement" msgstr "Name der Anforderung" -#: AKModel/models.py:629 AKModel/models.py:1129 +#: AKModel/models.py:638 AKModel/models.py:1137 msgid "AK Requirement" msgstr "AK-Anforderung" -#: AKModel/models.py:630 +#: AKModel/models.py:639 msgid "AK Requirements" msgstr "AK-Anforderungen" -#: AKModel/models.py:641 +#: AKModel/models.py:650 msgid "Name of the AK" msgstr "Name des AKs" -#: AKModel/models.py:642 +#: AKModel/models.py:651 msgid "Short Name" msgstr "Kurzer Name" -#: AKModel/models.py:643 +#: AKModel/models.py:652 msgid "Name displayed in the schedule" msgstr "Name zur Anzeige im AK-Plan" -#: AKModel/models.py:644 +#: AKModel/models.py:653 msgid "Description of the AK" msgstr "Beschreibung des AKs" -#: AKModel/models.py:646 +#: AKModel/models.py:655 msgid "Owners" msgstr "Leitungen" -#: AKModel/models.py:647 +#: AKModel/models.py:656 msgid "Those organizing the AK" msgstr "Menschen, die den AK organisieren und halten" -#: AKModel/models.py:650 +#: AKModel/models.py:659 msgid "Link to wiki page" msgstr "Link zur Wiki Seite" -#: AKModel/models.py:651 +#: AKModel/models.py:660 msgid "Protocol Link" msgstr "Protokolllink" -#: AKModel/models.py:651 +#: AKModel/models.py:660 msgid "Link to protocol" msgstr "Link zum Protokoll" -#: AKModel/models.py:653 +#: AKModel/models.py:662 msgid "Category" msgstr "Kategorie" -#: AKModel/models.py:654 +#: AKModel/models.py:663 msgid "Category of the AK" msgstr "Kategorie des AKs" -#: AKModel/models.py:655 +#: AKModel/models.py:664 msgid "Track" msgstr "Track" -#: AKModel/models.py:656 +#: AKModel/models.py:665 msgid "Track the AK belongs to" msgstr "Track zu dem der AK gehört" -#: AKModel/models.py:658 +#: AKModel/models.py:667 msgid "Resolution Intention" msgstr "Resolutionsabsicht" -#: AKModel/models.py:659 +#: AKModel/models.py:668 msgid "Intends to submit a resolution" msgstr "Beabsichtigt eine Resolution einzureichen" -#: AKModel/models.py:660 +#: AKModel/models.py:669 msgid "Present this AK" msgstr "AK präsentieren" -#: AKModel/models.py:661 +#: AKModel/models.py:670 msgid "Present results of this AK" msgstr "Die Ergebnisse dieses AKs vorstellen" -#: AKModel/models.py:663 AKModel/models.py:716 AKModel/views/status.py:170 +#: AKModel/models.py:672 AKModel/models.py:725 AKModel/views/status.py:170 msgid "Requirements" msgstr "Anforderungen" -#: AKModel/models.py:664 +#: AKModel/models.py:673 msgid "AK's Requirements" msgstr "Anforderungen des AKs" -#: AKModel/models.py:666 +#: AKModel/models.py:675 msgid "Conflicting AKs" msgstr "AK-Konflikte" -#: AKModel/models.py:667 +#: AKModel/models.py:676 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:668 +#: AKModel/models.py:677 msgid "Prerequisite AKs" msgstr "Vorausgesetzte AKs" -#: AKModel/models.py:669 +#: AKModel/models.py:678 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:671 +#: AKModel/models.py:680 msgid "Organizational Notes" msgstr "Notizen zur Organisation" -#: AKModel/models.py:672 +#: AKModel/models.py:681 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/" @@ -676,299 +688,299 @@ msgstr "" "Anmerkungen bitte den Button für Direktnachrichten verwenden (nach dem " "Anlegen/Bearbeiten)." -#: AKModel/models.py:675 AKModel/models.py:714 +#: AKModel/models.py:684 AKModel/models.py:723 msgid "Interest" msgstr "Interesse" -#: AKModel/models.py:675 +#: AKModel/models.py:684 msgid "Expected number of people" msgstr "Erwartete Personenzahl" -#: AKModel/models.py:676 +#: AKModel/models.py:685 msgid "Interest Counter" msgstr "Interessenszähler" -#: AKModel/models.py:677 +#: AKModel/models.py:686 msgid "People who have indicated interest online" msgstr "Anzahl Personen, die online Interesse bekundet haben" -#: AKModel/models.py:682 +#: AKModel/models.py:691 msgid "Export?" msgstr "Export?" -#: AKModel/models.py:683 +#: AKModel/models.py:692 msgid "Include AK in wiki export?" msgstr "AK bei Wiki-Export berücksichtigen?" -#: AKModel/models.py:718 +#: AKModel/models.py:727 msgid "Conflicts" msgstr "Konflikte" -#: AKModel/models.py:720 +#: AKModel/models.py:729 msgid "Prerequisites" msgstr "Voraussetzungen" -#: AKModel/models.py:813 +#: AKModel/models.py:822 msgid "Name or number of the room" msgstr "Name oder Nummer des Raums" -#: AKModel/models.py:814 +#: AKModel/models.py:823 msgid "Location" msgstr "Ort" -#: AKModel/models.py:815 +#: AKModel/models.py:824 msgid "Name or number of the location" msgstr "Name oder Nummer des Ortes" -#: AKModel/models.py:816 +#: AKModel/models.py:825 msgid "Capacity" msgstr "Kapazität" -#: AKModel/models.py:817 +#: AKModel/models.py:826 msgid "Maximum number of people (-1 for unlimited)." msgstr "Maximale Personenzahl (-1 wenn unbeschränkt)." -#: AKModel/models.py:818 +#: AKModel/models.py:827 msgid "Properties" msgstr "Eigenschaften" -#: AKModel/models.py:819 +#: AKModel/models.py:828 msgid "AK requirements fulfilled by the room" msgstr "AK-Anforderungen, die dieser Raum erfüllt" -#: AKModel/models.py:826 AKModel/views/status.py:59 +#: AKModel/models.py:835 AKModel/views/status.py:59 msgid "Rooms" msgstr "Räume" -#: AKModel/models.py:886 +#: AKModel/models.py:895 msgid "AK being mapped" msgstr "AK, der zugeordnet wird" -#: AKModel/models.py:888 +#: AKModel/models.py:897 msgid "Room the AK will take place in" msgstr "Raum in dem der AK stattfindet" -#: AKModel/models.py:889 AKModel/models.py:1304 +#: AKModel/models.py:898 AKModel/models.py:1312 msgid "Slot Begin" msgstr "Beginn des Slots" -#: AKModel/models.py:889 AKModel/models.py:1304 +#: AKModel/models.py:898 AKModel/models.py:1312 msgid "Time and date the slot begins" msgstr "Zeit und Datum zu der der AK beginnt" -#: AKModel/models.py:891 +#: AKModel/models.py:900 msgid "Duration" msgstr "Dauer" -#: AKModel/models.py:892 +#: AKModel/models.py:901 msgid "Length in hours" msgstr "Länge in Stunden" -#: AKModel/models.py:894 +#: AKModel/models.py:903 msgid "Scheduling fixed" msgstr "Planung fix" -#: AKModel/models.py:895 +#: AKModel/models.py:904 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:900 +#: AKModel/models.py:909 msgid "Last update" msgstr "Letzte Aktualisierung" -#: AKModel/models.py:903 +#: AKModel/models.py:912 msgid "AK Slot" msgstr "AK-Slot" -#: AKModel/models.py:904 AKModel/models.py:1122 AKModel/models.py:1157 +#: AKModel/models.py:913 AKModel/models.py:1130 AKModel/models.py:1165 msgid "AK Slots" msgstr "AK-Slot" -#: AKModel/models.py:926 AKModel/models.py:935 +#: AKModel/models.py:935 AKModel/models.py:944 msgid "Not scheduled yet" msgstr "Noch nicht geplant" -#: AKModel/models.py:1055 +#: AKModel/models.py:1063 msgid "AK this message belongs to" msgstr "AK zu dem die Nachricht gehört" -#: AKModel/models.py:1056 +#: AKModel/models.py:1064 msgid "Message text" msgstr "Nachrichtentext" -#: AKModel/models.py:1057 +#: AKModel/models.py:1065 msgid "Message to the organizers. This is not publicly visible." msgstr "" "Nachricht an die Organisator*innen. Diese ist nicht öffentlich sichtbar." -#: AKModel/models.py:1061 +#: AKModel/models.py:1069 msgid "Resolved" msgstr "Erledigt" -#: AKModel/models.py:1062 +#: AKModel/models.py:1070 msgid "This message has been resolved (no further action needed)" msgstr "" "Diese Nachricht wurde vollständig bearbeitet (keine weiteren Aktionen " "notwendig)" -#: AKModel/models.py:1065 +#: AKModel/models.py:1073 msgid "AK Orga Message" msgstr "AK-Organachricht" -#: AKModel/models.py:1066 +#: AKModel/models.py:1074 msgid "AK Orga Messages" msgstr "AK-Organachrichten" -#: AKModel/models.py:1083 +#: AKModel/models.py:1091 msgid "Constraint Violation" msgstr "Constraintverletzung" -#: AKModel/models.py:1084 +#: AKModel/models.py:1092 msgid "Constraint Violations" msgstr "Constraintverletzungen" -#: AKModel/models.py:1091 +#: AKModel/models.py:1099 msgid "Owner has two parallel slots" msgstr "Leitung hat zwei Slots parallel" -#: AKModel/models.py:1092 +#: AKModel/models.py:1100 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:1093 +#: AKModel/models.py:1101 msgid "Room has two AK slots scheduled at the same time" msgstr "Raum hat zwei AK Slots gleichzeitig" -#: AKModel/models.py:1094 +#: AKModel/models.py:1102 msgid "Room does not satisfy the requirement of the scheduled AK" msgstr "Room erfüllt die Anforderungen des platzierten AKs nicht" -#: AKModel/models.py:1095 +#: AKModel/models.py:1103 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:1096 +#: AKModel/models.py:1104 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:1098 +#: AKModel/models.py:1106 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:1099 +#: AKModel/models.py:1107 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:1100 +#: AKModel/models.py:1108 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:1101 +#: AKModel/models.py:1109 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:1102 +#: AKModel/models.py:1110 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:1108 +#: AKModel/models.py:1116 msgid "Warning" msgstr "Warnung" -#: AKModel/models.py:1109 +#: AKModel/models.py:1117 msgid "Violation" msgstr "Verletzung" -#: AKModel/models.py:1111 +#: AKModel/models.py:1119 msgid "Type" msgstr "Art" -#: AKModel/models.py:1112 +#: AKModel/models.py:1120 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:1113 +#: AKModel/models.py:1121 msgid "Level" msgstr "Level" -#: AKModel/models.py:1114 +#: AKModel/models.py:1122 msgid "Severity level of the violation" msgstr "Schweregrad der Verletzung" -#: AKModel/models.py:1121 +#: AKModel/models.py:1129 msgid "AK(s) belonging to this constraint" msgstr "AK(s), die zu diesem Constraint gehören" -#: AKModel/models.py:1123 +#: AKModel/models.py:1131 msgid "AK Slot(s) belonging to this constraint" msgstr "AK Slot(s), die zu diesem Constraint gehören" -#: AKModel/models.py:1125 +#: AKModel/models.py:1133 msgid "AK Owner belonging to this constraint" msgstr "AK Leitung(en), die zu diesem Constraint gehören" -#: AKModel/models.py:1127 +#: AKModel/models.py:1135 msgid "Room belonging to this constraint" msgstr "Raum, der zu diesem Constraint gehört" -#: AKModel/models.py:1130 +#: AKModel/models.py:1138 msgid "AK Requirement belonging to this constraint" msgstr "AK Anforderung, die zu diesem Constraint gehört" -#: AKModel/models.py:1132 +#: AKModel/models.py:1140 msgid "AK Category belonging to this constraint" msgstr "AK Kategorie, di zu diesem Constraint gehört" -#: AKModel/models.py:1134 +#: AKModel/models.py:1142 msgid "Comment" msgstr "Kommentar" -#: AKModel/models.py:1134 +#: AKModel/models.py:1142 msgid "Comment or further details for this violation" msgstr "Kommentar oder weitere Details zu dieser Vereletzung" -#: AKModel/models.py:1137 +#: AKModel/models.py:1145 msgid "Timestamp" msgstr "Timestamp" -#: AKModel/models.py:1137 +#: AKModel/models.py:1145 msgid "Time of creation" msgstr "Zeitpunkt der ERstellung" -#: AKModel/models.py:1138 +#: AKModel/models.py:1146 msgid "Manually Resolved" msgstr "Manuell behoben" -#: AKModel/models.py:1139 +#: AKModel/models.py:1147 msgid "Mark this violation manually as resolved" msgstr "Markiere diese Verletzung manuell als behoben" -#: AKModel/models.py:1166 +#: AKModel/models.py:1174 #: AKModel/templates/admin/AKModel/requirements_overview.html:27 msgid "Details" msgstr "Details" -#: AKModel/models.py:1300 +#: AKModel/models.py:1308 msgid "Default Slot" msgstr "Standardslot" -#: AKModel/models.py:1305 +#: AKModel/models.py:1313 msgid "Slot End" msgstr "Ende des Slots" -#: AKModel/models.py:1305 +#: AKModel/models.py:1313 msgid "Time and date the slot ends" msgstr "Zeit und Datum zu der der Slot endet" -#: AKModel/models.py:1310 +#: AKModel/models.py:1318 msgid "Primary categories" msgstr "Primäre Kategorien" -#: AKModel/models.py:1311 +#: AKModel/models.py:1319 msgid "Categories that should be assigned to this slot primarily" msgstr "Kategorieren, die diesem Slot primär zugewiesen werden sollen" @@ -1178,35 +1190,35 @@ msgstr "AK-CSV-Export" msgid "AK JSON Export" msgstr "AK-JSON-Export" -#: AKModel/views/ak.py:190 +#: AKModel/views/ak.py:184 msgid "AK Wiki Export" msgstr "AK-Wiki-Export" -#: AKModel/views/ak.py:201 AKModel/views/manage.py:55 +#: AKModel/views/ak.py:195 AKModel/views/manage.py:55 msgid "Wishes" msgstr "Wünsche" -#: AKModel/views/ak.py:213 +#: AKModel/views/ak.py:207 msgid "Delete AK Orga Messages" msgstr "AK-Organachrichten löschen" -#: AKModel/views/ak.py:231 +#: AKModel/views/ak.py:225 msgid "AK Orga Messages successfully deleted" msgstr "AK-Organachrichten erfolgreich gelöscht" -#: AKModel/views/ak.py:243 +#: AKModel/views/ak.py:237 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:244 +#: AKModel/views/ak.py:238 msgid "Reset of interest in AKs successful." msgstr "Interesse an AKs erfolgreich zurückgesetzt." -#: AKModel/views/ak.py:258 +#: AKModel/views/ak.py:252 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:259 +#: AKModel/views/ak.py:253 msgid "AKs' interest counters set back to 0." msgstr "Interessenszähler der AKs zurückgesetzt" -- GitLab From 8501afaf52df8c23716109cb7d6c9270e5fbb363 Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Thu, 6 Feb 2025 17:46:26 +0100 Subject: [PATCH 61/77] Address line length --- AKModel/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AKModel/models.py b/AKModel/models.py index b3820a15..e3530861 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -153,8 +153,8 @@ class Event(models.Model): 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.' + 'Slot duration in hours that is used in the timeslot discretization, ' + 'when this event is exported for the solver.' )) -- GitLab From ed98da8ded352f56fc838552d7133839423c916b Mon Sep 17 00:00:00 2001 From: Lorenzo Conti <lorenzo@uni-bonn.de> Date: Thu, 6 Feb 2025 18:14:09 +0000 Subject: [PATCH 62/77] Apply 3 suggestion(s) to 2 file(s) Co-authored-by: Felix Blanke <s6feblan@uni-bonn.de> --- AKModel/models.py | 4 ++-- AKModel/views/ak.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/AKModel/models.py b/AKModel/models.py index e3530861..8601a992 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -402,7 +402,7 @@ class Event(models.Model): """ if slots_in_an_hour is None: - slots_in_an_hour = float(self.export_slot) + slots_in_an_hour = 1.0 / float(self.export_slot) if DefaultSlot.objects.filter(event=self).exists(): # discretize default slots if they exists @@ -1018,7 +1018,7 @@ class AKSlot(models.Model): data = { "id": str(self.pk), - "duration": math.ceil(self.duration * self.event.export_slot - ceil_offet_eps), + "duration": math.ceil(self.duration / self.event.export_slot - ceil_offet_eps), "properties": { "conflicts": [str(conflict.pk) for conflict in conflict_slots.all()] diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index d9a5e6a8..f6fd7932 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -92,7 +92,7 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): context["rooms"] = rooms timeslots = { - "info": {"duration": (1.0 / float(self.event.export_slot)), }, + "info": {"duration": float(self.event.export_slot)}, "blocks": [], } -- GitLab From dafdcf591994c91df1d0ea001a560a48862e3280 Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Sat, 8 Feb 2025 12:59:44 +0100 Subject: [PATCH 63/77] If no owner availability is given, assume avail for full event --- AKModel/models.py | 3 ++- AKModel/tests/test_json_export.py | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/AKModel/models.py b/AKModel/models.py index 8601a992..f11c66e2 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -1006,7 +1006,8 @@ class AKSlot(models.Model): ak_time_constraints = [] def _owner_time_constraints(owner: AKOwner): - if Availability.is_event_covered(self.event, owner.availabilities.all()): + owner_avails = owner.availabilities.all() + if not owner_avails or Availability.is_event_covered(self.event, owner_avails): return [] return [f"availability-person-{owner.pk}"] diff --git a/AKModel/tests/test_json_export.py b/AKModel/tests/test_json_export.py index d53cd4c5..5a604dc7 100644 --- a/AKModel/tests/test_json_export.py +++ b/AKModel/tests/test_json_export.py @@ -479,6 +479,10 @@ class JSONExportTest(TestCase): for owner in slot.ak.owners.all(): # restricted owner availability + if not owner.availabilities.all(): + # no availability for owner -> assume full event is covered + continue + if not Availability.is_event_covered( slot.event, owner.availabilities.all() ): -- GitLab From 97d3bb1f5ca9d27dec669450bfc23dc092bdb429 Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Sun, 9 Feb 2025 23:52:03 +0100 Subject: [PATCH 64/77] Store IDs as integer in JSON export --- AKModel/models.py | 12 +++--- AKModel/tests/test_json_export.py | 67 +++++++++++++++++-------------- AKModel/views/ak.py | 2 +- 3 files changed, 43 insertions(+), 38 deletions(-) diff --git a/AKModel/models.py b/AKModel/models.py index f11c66e2..5bd570dc 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -871,7 +871,7 @@ class Room(models.Model): time_constraints = [f"availability-room-{self.pk}"] data = { - "id": str(self.pk), + "id": self.pk, "info": { "name": self.name, }, @@ -1018,13 +1018,13 @@ class AKSlot(models.Model): ceil_offet_eps = decimal.Decimal(1e-4) data = { - "id": str(self.pk), + "id": self.pk, "duration": math.ceil(self.duration / self.event.export_slot - ceil_offet_eps), "properties": { "conflicts": - [str(conflict.pk) for conflict in conflict_slots.all()] - + [str(second_slot.pk) for second_slot in other_ak_slots.all()], - "dependencies": [str(dep.pk) for dep in dependency_slots.all()], + [conflict.pk for conflict in conflict_slots.all()] + + [second_slot.pk for second_slot in other_ak_slots.all()], + "dependencies": [dep.pk for dep in dependency_slots.all()], }, "room_constraints": [constraint.name for constraint in self.ak.requirements.all()], @@ -1036,7 +1036,7 @@ class AKSlot(models.Model): "description": self.ak.description, "reso": self.ak.reso, "duration_in_hours": float(self.duration), - "django_ak_id": str(self.ak.pk), + "django_ak_id": self.ak.pk, }, } diff --git a/AKModel/tests/test_json_export.py b/AKModel/tests/test_json_export.py index 5a604dc7..ee3774be 100644 --- a/AKModel/tests/test_json_export.py +++ b/AKModel/tests/test_json_export.py @@ -89,7 +89,7 @@ class JSONExportTest(TestCase): with self.subTest(event=event): self.set_up_event(event=event) self.assertEqual( - {str(slot.pk) for slot in self.ak_slots}, + {slot.pk for slot in self.ak_slots}, self.export_objects["aks"].keys(), "Exported AKs does not match the AKSlots of the event", ) @@ -152,7 +152,7 @@ class JSONExportTest(TestCase): f"{item} properties keys not as expected", ) - self._check_type(ak["id"], str, "id", item=item) + self._check_type(ak["id"], int, "id", item=item) self._check_type(ak["duration"], int, "duration", item=item) self._check_type(ak["info"]["name"], str, "info/name", item=item) self._check_type(ak["info"]["head"], str, "info/head", item=item) @@ -168,16 +168,22 @@ class JSONExportTest(TestCase): ) self._check_type( ak["info"]["django_ak_id"], - str, + int, "info/django_ak_id", item=item, ) self._check_lst( - ak["properties"]["conflicts"], "conflicts", item=item + ak["properties"]["conflicts"], + "conflicts", + item=item, + contained_type=int, ) self._check_lst( - ak["properties"]["dependencies"], "dependencies", item=item + ak["properties"]["dependencies"], + "dependencies", + item=item, + contained_type=int, ) self._check_lst( ak["time_constraints"], "time_constraints", item=item @@ -212,7 +218,7 @@ class JSONExportTest(TestCase): f"{item} info keys not as expected", ) - self._check_type(room["id"], str, "id", item=item) + self._check_type(room["id"], int, "id", item=item) self._check_type(room["capacity"], int, "capacity", item=item) self._check_type(room["info"]["name"], str, "info/name", item=item) @@ -279,7 +285,7 @@ class JSONExportTest(TestCase): {"start", "end"}, f"{item} info keys not as expected", ) - self._check_type(timeslot["id"], str, "id", item=item) + self._check_type(timeslot["id"], int, "id", item=item) self._check_type( timeslot["info"]["start"], str, "info/start", item=item ) @@ -292,10 +298,10 @@ class JSONExportTest(TestCase): if prev_id is not None: self.assertLess( prev_id, - int(timeslot["id"]), + timeslot["id"], "timeslot ids must be increasing", ) - prev_id = int(timeslot["id"]) + prev_id = timeslot["id"] def test_general_conformity_to_spec(self): """Test if rest of JSON structure and types conform to standard.""" @@ -332,7 +338,7 @@ class JSONExportTest(TestCase): self.set_up_event(event=event) for slot in self.ak_slots: - ak = self.export_objects["aks"][str(slot.pk)] + ak = self.export_objects["aks"][slot.pk] self.assertLessEqual( float(slot.duration) * self.slots_in_an_hour - 1e-4, @@ -359,23 +365,22 @@ class JSONExportTest(TestCase): self.set_up_event(event=event) for slot in self.ak_slots: - ak = self.export_objects["aks"][str(slot.pk)] - conflict_slots = self.ak_slots.filter( - ak__in=slot.ak.conflicts.all() - ).values_list("pk", flat=True) - conflict_pks = {str(conflict_pk) for conflict_pk in conflict_slots} + ak = self.export_objects["aks"][slot.pk] + conflict_slots = set( + self.ak_slots.filter( + ak__in=slot.ak.conflicts.all() + ).values_list("pk", flat=True) + ) other_ak_slots = ( self.ak_slots.filter(ak=slot.ak) .exclude(pk=slot.pk) .values_list("pk", flat=True) ) - conflict_pks.update( - str(other_slot_pk) for other_slot_pk in other_ak_slots - ) + conflict_slots.update(other_ak_slots) self.assertEqual( - conflict_pks, + conflict_slots, set(ak["properties"]["conflicts"]), f"Conflicts for slot {slot.pk} not as expected", ) @@ -387,13 +392,13 @@ class JSONExportTest(TestCase): self.set_up_event(event=event) for slot in self.ak_slots: - ak = self.export_objects["aks"][str(slot.pk)] + ak = self.export_objects["aks"][slot.pk] dependency_slots = self.ak_slots.filter( ak__in=slot.ak.prerequisites.all() ).values_list("pk", flat=True) self.assertEqual( - {str(dep_pk) for dep_pk in dependency_slots}, + set(dependency_slots), set(ak["properties"]["dependencies"]), f"Dependencies for slot {slot.pk} not as expected", ) @@ -405,7 +410,7 @@ class JSONExportTest(TestCase): self.set_up_event(event=event) for slot in self.ak_slots: - ak = self.export_objects["aks"][str(slot.pk)] + ak = self.export_objects["aks"][slot.pk] self.assertEqual(slot.ak.reso, ak["info"]["reso"]) self.assertEqual( slot.ak.reso, "resolution" in ak["time_constraints"] @@ -418,13 +423,13 @@ class JSONExportTest(TestCase): self.set_up_event(event=event) for slot in self.ak_slots: - ak = self.export_objects["aks"][str(slot.pk)] + ak = self.export_objects["aks"][slot.pk] self.assertEqual(ak["info"]["name"], slot.ak.name) self.assertEqual( ak["info"]["head"], ", ".join(map(str, slot.ak.owners.all())) ) self.assertEqual(ak["info"]["description"], slot.ak.description) - self.assertEqual(ak["info"]["django_ak_id"], str(slot.ak.pk)) + self.assertEqual(ak["info"]["django_ak_id"], slot.ak.pk) def test_ak_room_constraints(self): """Test if AK room constraints are exported as expected.""" @@ -433,7 +438,7 @@ class JSONExportTest(TestCase): self.set_up_event(event=event) for slot in self.ak_slots: - ak = self.export_objects["aks"][str(slot.pk)] + ak = self.export_objects["aks"][slot.pk] requirements = list( slot.ak.requirements.values_list("name", flat=True) ) @@ -488,7 +493,7 @@ class JSONExportTest(TestCase): ): time_constraints.add(f"availability-person-{owner.pk}") - ak = self.export_objects["aks"][str(slot.pk)] + ak = self.export_objects["aks"][slot.pk] self.assertEqual( set(ak["time_constraints"]), time_constraints, @@ -502,7 +507,7 @@ class JSONExportTest(TestCase): self.set_up_event(event=event) self.assertEqual( - {str(room.pk) for room in self.rooms}, + {room.pk for room in self.rooms}, self.export_objects["rooms"].keys(), "Exported Rooms do not match the Rooms of the event", ) @@ -514,7 +519,7 @@ class JSONExportTest(TestCase): self.set_up_event(event=event) for room in self.rooms: - export_room = self.export_objects["rooms"][str(room.pk)] + export_room = self.export_objects["rooms"][room.pk] self.assertEqual(room.capacity, export_room["capacity"]) def test_room_info(self): @@ -524,7 +529,7 @@ class JSONExportTest(TestCase): self.set_up_event(event=event) for room in self.rooms: - export_room = self.export_objects["rooms"][str(room.pk)] + export_room = self.export_objects["rooms"][room.pk] self.assertEqual(room.name, export_room["info"]["name"]) def test_room_timeconstraints(self): @@ -542,7 +547,7 @@ class JSONExportTest(TestCase): ): time_constraints.add(f"availability-room-{room.pk}") - export_room = self.export_objects["rooms"][str(room.pk)] + export_room = self.export_objects["rooms"][room.pk] self.assertEqual( time_constraints, set(export_room["time_constraints"]) ) @@ -568,7 +573,7 @@ class JSONExportTest(TestCase): fulfilled_room_constraints.add(f"fixed-room-{room.pk}") - export_room = self.export_objects["rooms"][str(room.pk)] + export_room = self.export_objects["rooms"][room.pk] self.assertEqual( fulfilled_room_constraints, set(export_room["fulfilled_room_constraints"]), diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index f6fd7932..d2f17676 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -147,7 +147,7 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): time_constraints.extend(timeslot.constraints) current_block.append({ - "id": str(timeslot.idx), + "id": timeslot.idx, "info": { "start": timeslot.avail.start.astimezone(self.event.timezone).strftime("%Y-%m-%d %H:%M"), "end": timeslot.avail.end.astimezone(self.event.timezone).strftime("%Y-%m-%d %H:%M"), -- GitLab From 444e22061cef65960ef19e203e5ac8d1e82fb002 Mon Sep 17 00:00:00 2001 From: Felix Blanke <s6feblan@uni-bonn.de> Date: Tue, 11 Feb 2025 11:20:04 +0000 Subject: [PATCH 65/77] Expand block info in JSON export --- AKModel/tests/test_json_export.py | 187 ++++++++++++++++++++---------- AKModel/views/ak.py | 27 ++++- 2 files changed, 150 insertions(+), 64 deletions(-) diff --git a/AKModel/tests/test_json_export.py b/AKModel/tests/test_json_export.py index 5a604dc7..48c8d7fd 100644 --- a/AKModel/tests/test_json_export.py +++ b/AKModel/tests/test_json_export.py @@ -248,7 +248,7 @@ class JSONExportTest(TestCase): ) self.assertEqual( self.export_dict["timeslots"]["info"].keys(), - {"duration"}, + {"duration", "blocknames"}, "timeslot info keys not as expected", ) self._check_type( @@ -257,6 +257,21 @@ class JSONExportTest(TestCase): "info/duration", item=item, ) + self._check_lst( + self.export_dict["timeslots"]["info"]["blocknames"], + "info/blocknames", + item=item, + contained_type=list, + ) + for blockname in self.export_dict["timeslots"]["info"]["blocknames"]: + self.assertEqual(len(blockname), 2) + self._check_lst( + blockname, + "info/blocknames/entry", + item=item, + contained_type=str, + ) + self._check_lst( self.export_dict["timeslots"]["blocks"], "blocks", @@ -710,72 +725,120 @@ class JSONExportTest(TestCase): self.set_up_event(event=event) cat_avails = self._get_cat_availability() - for timeslot in chain.from_iterable( + num_blocks = len(self.export_dict["timeslots"]["blocks"]) + for block_idx, block in enumerate( self.export_dict["timeslots"]["blocks"] ): - start, end = self._get_timeslot_start_end(timeslot) - timeslot_avail = Availability( - event=self.event, start=start, end=end - ) - - fulfilled_time_constraints = set() + for timeslot in block: + start, end = self._get_timeslot_start_end(timeslot) + timeslot_avail = Availability( + event=self.event, start=start, end=end + ) - # reso deadline - if self.event.reso_deadline is not None: - # timeslot ends before deadline - if end < self.event.reso_deadline.astimezone( - self.event.timezone - ): - fulfilled_time_constraints.add("resolution") - - # add category constraints - fulfilled_time_constraints |= ( - AKCategory.create_category_constraints( - [ - cat - for cat in AKCategory.objects.filter( - event=self.event - ).all() - if timeslot_avail.is_covered(cat_avails[cat.name]) - ] + fulfilled_time_constraints = set() + + # reso deadline + if self.event.reso_deadline is not None: + # timeslot ends before deadline + if end < self.event.reso_deadline.astimezone( + self.event.timezone + ): + fulfilled_time_constraints.add("resolution") + + # add category constraints + fulfilled_time_constraints |= ( + AKCategory.create_category_constraints( + [ + cat + for cat in AKCategory.objects.filter( + event=self.event + ).all() + if timeslot_avail.is_covered(cat_avails[cat.name]) + ] + ) ) - ) - # add owner constraints - fulfilled_time_constraints |= { - f"availability-person-{owner.id}" - for owner in AKOwner.objects.filter(event=self.event).all() - if self._is_restricted_and_contained_slot( - timeslot_avail, - Availability.union(owner.availabilities.all()), + # add owner constraints + fulfilled_time_constraints |= { + f"availability-person-{owner.id}" + for owner in AKOwner.objects.filter(event=self.event).all() + if self._is_restricted_and_contained_slot( + timeslot_avail, + Availability.union(owner.availabilities.all()), + ) + } + + # add room constraints + fulfilled_time_constraints |= { + f"availability-room-{room.id}" + for room in self.rooms + if self._is_restricted_and_contained_slot( + timeslot_avail, + Availability.union(room.availabilities.all()), + ) + } + + # add ak constraints + fulfilled_time_constraints |= { + f"availability-ak-{ak.id}" + for ak in AK.objects.filter(event=event) + if self._is_restricted_and_contained_slot( + timeslot_avail, + Availability.union(ak.availabilities.all()), + ) + } + fulfilled_time_constraints |= { + f"fixed-akslot-{slot.id}" + for slot in self.ak_slots + if self._is_ak_fixed_in_slot(slot, timeslot_avail) + } + + fulfilled_time_constraints |= { + f"notblock{idx}" + for idx in range(num_blocks) + if idx != block_idx + } + + self.assertEqual( + fulfilled_time_constraints, + set(timeslot["fulfilled_time_constraints"]), ) - } - - # add room constraints - fulfilled_time_constraints |= { - f"availability-room-{room.id}" - for room in self.rooms - if self._is_restricted_and_contained_slot( - timeslot_avail, - Availability.union(room.availabilities.all()), + + def test_timeslots_info(self): + """Test timeslots info dict""" + for event in Event.objects.all(): + with self.subTest(event=event): + self.set_up_event(event=event) + + self.assertAlmostEqual( + self.export_dict["timeslots"]["info"]["duration"], + float(self.event.export_slot), + ) + + block_names = [] + for block in self.export_dict["timeslots"]["blocks"]: + if not block: + continue + + block_start, _ = self._get_timeslot_start_end(block[0]) + _, block_end = self._get_timeslot_start_end(block[-1]) + + start_day = block_start.strftime("%A, %d. %b") + if block_start.date() == block_end.date(): + # same day + time_str = ( + block_start.strftime("%H:%M") + + " – " + + block_end.strftime("%H:%M") ) - } - - # add ak constraints - fulfilled_time_constraints |= { - f"availability-ak-{ak.id}" - for ak in AK.objects.filter(event=event) - if self._is_restricted_and_contained_slot( - timeslot_avail, Availability.union(ak.availabilities.all()) + else: + # different days + time_str = ( + block_start.strftime("%a %H:%M") + + " – " + + block_end.strftime("%a %H:%M") ) - } - fulfilled_time_constraints |= { - f"fixed-akslot-{slot.id}" - for slot in self.ak_slots - if self._is_ak_fixed_in_slot(slot, timeslot_avail) - } - - self.assertEqual( - fulfilled_time_constraints, - set(timeslot["fulfilled_time_constraints"]), - ) + block_names.append([start_day, time_str]) + self.assertEqual( + block_names, self.export_dict["timeslots"]["info"]["blocknames"] + ) diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index f6fd7932..f8b83f04 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -1,4 +1,5 @@ import json +from datetime import datetime from typing import List from django.contrib import messages @@ -109,11 +110,30 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): for person in AKOwner.objects.filter(event=self.event) } - blocks = self.event.discretize_timeslots() + blocks = list(self.event.discretize_timeslots()) - for block in blocks: + block_names = [] + + for block_idx, block in enumerate(blocks): current_block = [] + if not block: + continue + + block_start = block[0].avail.start.astimezone(self.event.timezone) + block_end = block[-1].avail.end.astimezone(self.event.timezone) + + start_day = block_start.strftime("%A, %d. %b") + if block_start.date() == block_end.date(): + # same day + time_str = block_start.strftime("%H:%M") + " – " + block_end.strftime("%H:%M") + else: + # different days + time_str = block_start.strftime("%a %H:%M") + " – " + block_end.strftime("%a %H:%M") + block_names.append([start_day, time_str]) + + block_timeconstraints = [f"notblock{idx}" for idx in range(len(blocks)) if idx != block_idx] + for timeslot in block: time_constraints = [] # if reso_deadline is set and timeslot ends before it, @@ -145,6 +165,7 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): ]) time_constraints.extend(timeslot.constraints) + time_constraints.extend(block_timeconstraints) current_block.append({ "id": str(timeslot.idx), @@ -157,6 +178,8 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): timeslots["blocks"].append(current_block) + timeslots["info"]["blocknames"] = block_names + context["timeslots"] = json.dumps(timeslots) info_dict = { -- GitLab From abbe82a1862b07e1826dfdfc0664970850d77cc7 Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Wed, 26 Feb 2025 13:59:49 +0100 Subject: [PATCH 66/77] Address lint remarks --- AKModel/models.py | 1 + AKModel/views/ak.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/AKModel/models.py b/AKModel/models.py index 5bd570dc..7c1a920b 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -273,6 +273,7 @@ class Event(models.Model): start: datetime, end: datetime, slot_duration: timedelta, + *, slot_index: int = 0, constraints: set[str] | None = None, ) -> Generator[TimeslotBlock, None, int]: diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index 43fe489b..40a6e102 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -1,5 +1,4 @@ import json -from datetime import datetime from typing import List from django.contrib import messages -- GitLab From a54da7ac0b8988674542e850259ea9b632b7b48f Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Mon, 10 Feb 2025 17:03:00 +0100 Subject: [PATCH 67/77] Change funcs to export json dict instead of str --- AKModel/models.py | 10 +++++----- .../templates/admin/AKModel/ak_json_export.html | 13 +------------ AKModel/views/ak.py | 14 +++++++++----- 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/AKModel/models.py b/AKModel/models.py index 7c1a920b..4b3a3ceb 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -4,7 +4,7 @@ import json import math from dataclasses import dataclass from datetime import datetime, timedelta -from typing import Iterable, Generator +from typing import Any, Iterable, Generator from django.db import models, transaction from django.apps import apps @@ -852,7 +852,7 @@ class Room(models.Model): def __str__(self): return self.title - def as_json(self) -> str: + def as_json_dict(self) -> dict[str, Any]: """Return a json string representation of this room object. :return: The json string representation is constructed @@ -887,7 +887,7 @@ class Room(models.Model): if not any(constr.startswith("proxy") for constr in data["fulfilled_room_constraints"]): data["fulfilled_room_constraints"].append("no-proxy") - return json.dumps(data) + return data class AKSlot(models.Model): @@ -984,7 +984,7 @@ 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: + def as_json_dict(self) -> dict[str, Any]: """Return a json string representation of the AK object of this slot. :return: The json string representation is constructed @@ -1055,7 +1055,7 @@ class AKSlot(models.Model): if not any(constr.startswith("proxy") for constr in data["room_constraints"]): data["room_constraints"].append("no-proxy") - return json.dumps(data) + return 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 index 38e5526e..7ae242ca 100644 --- a/AKModel/templates/admin/AKModel/ak_json_export.html +++ b/AKModel/templates/admin/AKModel/ak_json_export.html @@ -4,17 +4,6 @@ {% 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 }} - } +{{ json_data }} </pre> {% endblock %} diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index 40a6e102..32d840fb 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -86,10 +86,10 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["participants"] = json.dumps([]) + data = {} rooms = Room.objects.filter(event=self.event) - context["rooms"] = rooms + data["rooms"] = [r.as_json_dict() for r in rooms] timeslots = { "info": {"duration": float(self.event.export_slot)}, @@ -179,17 +179,21 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): timeslots["info"]["blocknames"] = block_names - 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) + data["timeslots"] = timeslots + data["info"] = info_dict + data["participants"] = [] + data["aks"] = [ak.as_json_dict() for ak in context["slots"]] + + context["json_data"] = json.dumps(data, indent=2) return context -- GitLab From 8ac9cd0bbe363d8bc8b083f1cc496f4061280a82 Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Mon, 10 Feb 2025 22:17:08 +0100 Subject: [PATCH 68/77] Add oneliner --- AKModel/templates/admin/AKModel/ak_json_export.html | 11 +++++++++++ AKModel/views/ak.py | 1 + 2 files changed, 12 insertions(+) diff --git a/AKModel/templates/admin/AKModel/ak_json_export.html b/AKModel/templates/admin/AKModel/ak_json_export.html index 7ae242ca..65e459a5 100644 --- a/AKModel/templates/admin/AKModel/ak_json_export.html +++ b/AKModel/templates/admin/AKModel/ak_json_export.html @@ -3,7 +3,18 @@ {% load tz %} {% block content %} + +<p> +Exported JSON: +<pre> +{{ json_data_oneline }} +</pre> +</p> + +<p> +Exported JSON (indented for better readability): <pre> {{ json_data }} </pre> +</p> {% endblock %} diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index 32d840fb..15abf0df 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -193,6 +193,7 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): data["participants"] = [] data["aks"] = [ak.as_json_dict() for ak in context["slots"]] + context["json_data_oneline"] = json.dumps(data) context["json_data"] = json.dumps(data, indent=2) return context -- GitLab From 12bce4441c0eec7772bd159d7ea1acd13582e9be Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Mon, 10 Feb 2025 22:20:25 +0100 Subject: [PATCH 69/77] refactor --- AKModel/views/ak.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index 15abf0df..518ddbb5 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -86,16 +86,14 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - data = {} - - rooms = Room.objects.filter(event=self.event) - data["rooms"] = [r.as_json_dict() for r in rooms] timeslots = { "info": {"duration": float(self.event.export_slot)}, "blocks": [], } + rooms = Room.objects.filter(event=self.event) + ak_availabilities = { ak.pk: Availability.union(ak.availabilities.all()) for ak in AK.objects.filter(event=self.event).all() @@ -188,10 +186,13 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): if hasattr(self.event, attr) and getattr(self.event, attr): info_dict[attr] = getattr(self.event, attr) - data["timeslots"] = timeslots - data["info"] = info_dict - data["participants"] = [] - data["aks"] = [ak.as_json_dict() for ak in context["slots"]] + data = { + "participants": [], + "rooms": [r.as_json_dict() for r in rooms], + "timeslots": timeslots, + "info": info_dict, + "aks": [ak.as_json_dict() for ak in context["slots"]], + } context["json_data_oneline"] = json.dumps(data) context["json_data"] = json.dumps(data, indent=2) -- GitLab From fb1905a59fb17610e290b16ae7a23981f1d7589e Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Wed, 26 Feb 2025 16:52:26 +0100 Subject: [PATCH 70/77] Move json dict assembly from view to model --- AKModel/models.py | 151 ++++++++++++++++++++++++++++++++++++++++++++ AKModel/views/ak.py | 144 +----------------------------------------- 2 files changed, 153 insertions(+), 142 deletions(-) diff --git a/AKModel/models.py b/AKModel/models.py index 4b3a3ceb..22e0012e 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -488,6 +488,157 @@ class Event(models.Model): return slots_updated + def as_json_dict(self) -> dict[str, Any]: + """Return the json representation of this Event. + + :return: The json dict 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: dict[str, Any] + """ + + # local import to prevent cyclic import + # pylint: disable=import-outside-toplevel + from AKModel.availability.models import Availability + + def _test_event_not_covered(availabilities: list[Availability]) -> bool: + """Test if event is not covered by availabilities.""" + return not Availability.is_event_covered(self, availabilities) + + def _test_akslot_fixed_in_timeslot(ak_slot: AKSlot, timeslot: Availability) -> bool: + """Test if an AKSlot is fixed to overlap a timeslot slot.""" + if not ak_slot.fixed or ak_slot.start is None: + return False + + fixed_avail = Availability(event=self, start=ak_slot.start, end=ak_slot.end) + return fixed_avail.overlaps(timeslot, strict=True) + + 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) + ) + + def _generate_time_constraints( + avail_label: str, + avail_dict: dict, + timeslot_avail: Availability, + prefix: str = "availability", + ) -> list[str]: + return [ + f"{prefix}-{avail_label}-{pk}" + for pk, availabilities in avail_dict.items() + if _test_add_constraint(timeslot_avail, availabilities) + ] + + timeslots = { + "info": {"duration": float(self.export_slot)}, + "blocks": [], + } + + rooms = Room.objects.filter(event=self) + slots = AKSlot.objects.filter(event=self) + + ak_availabilities = { + ak.pk: Availability.union(ak.availabilities.all()) + for ak in AK.objects.filter(event=self).all() + } + 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) + } + + blocks = list(self.discretize_timeslots()) + + block_names = [] + + for block_idx, block in enumerate(blocks): + current_block = [] + + if not block: + continue + + block_start = block[0].avail.start.astimezone(self.timezone) + block_end = block[-1].avail.end.astimezone(self.timezone) + + start_day = block_start.strftime("%A, %d. %b") + if block_start.date() == block_end.date(): + # same day + time_str = block_start.strftime("%H:%M") + " – " + block_end.strftime("%H:%M") + else: + # different days + time_str = block_start.strftime("%a %H:%M") + " – " + block_end.strftime("%a %H:%M") + block_names.append([start_day, time_str]) + + block_timeconstraints = [f"notblock{idx}" for idx in range(len(blocks)) if idx != block_idx] + + for timeslot in block: + time_constraints = [] + # if reso_deadline is set and timeslot ends before it, + # add fulfilled time constraint 'resolution' + if self.reso_deadline is None or timeslot.avail.end < self.reso_deadline: + time_constraints.append("resolution") + + # add fulfilled time constraints for all AKs that cannot happen during full event + time_constraints.extend( + _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) + ) + + # 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) + ) + + # add fulfilled time constraints for all AKSlots fixed to happen during timeslot + time_constraints.extend([ + f"fixed-akslot-{slot.id}" + for slot in AKSlot.objects.filter(event=self, fixed=True).exclude(start__isnull=True) + if _test_akslot_fixed_in_timeslot(slot, timeslot.avail) + ]) + + time_constraints.extend(timeslot.constraints) + time_constraints.extend(block_timeconstraints) + + current_block.append({ + "id": timeslot.idx, + "info": { + "start": timeslot.avail.start.astimezone(self.timezone).strftime("%Y-%m-%d %H:%M"), + "end": timeslot.avail.end.astimezone(self.timezone).strftime("%Y-%m-%d %H:%M"), + }, + "fulfilled_time_constraints": time_constraints, + }) + + timeslots["blocks"].append(current_block) + + timeslots["info"]["blocknames"] = block_names + + info_dict = { + "title": self.name, + "slug": self.slug + } + + for attr in ["contact_email", "place"]: + if hasattr(self, attr) and getattr(self, attr): + info_dict[attr] = getattr(self, attr) + + return { + "participants": [], + "rooms": [r.as_json_dict() for r in rooms], + "timeslots": timeslots, + "info": info_dict, + "aks": [ak.as_json_dict() for ak in slots], + } + + class AKOwner(models.Model): """ An AKOwner describes the person organizing/holding an AK. """ diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index 518ddbb5..b4af5a8f 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -1,15 +1,13 @@ 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, Room, AKOwner +from AKModel.models import AKRequirement, AKSlot, Event, AKOrgaMessage, AK class AKRequirementOverview(AdminViewMixin, FilterByEventSlugMixin, ListView): @@ -50,157 +48,19 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): context_object_name = "slots" title = _("AK JSON Export") - def _test_event_not_covered(self, availabilities: List[Availability]) -> bool: - """Test if event is not covered by availabilities.""" - return not Availability.is_event_covered(self.event, availabilities) - - def _test_akslot_fixed_in_timeslot(self, ak_slot: AKSlot, timeslot: Availability) -> bool: - """Test if an AKSlot is fixed to overlap a timeslot slot.""" - if not ak_slot.fixed or ak_slot.start is None: - return False - - fixed_avail = Availability(event=self.event, start=ak_slot.start, end=ak_slot.end) - return fixed_avail.overlaps(timeslot, strict=True) - - def _test_add_constraint(self, slot: Availability, availabilities: List[Availability]) -> bool: - """Test if object is not available for whole event and may happen during slot.""" - return ( - self._test_event_not_covered(availabilities) and slot.is_covered(availabilities) - ) - - def _generate_time_constraints( - self, - avail_label: str, - avail_dict: dict, - timeslot_avail: Availability, - prefix: str = "availability", - ) -> list[str]: - return [ - f"{prefix}-{avail_label}-{pk}" - for pk, availabilities in avail_dict.items() - if self._test_add_constraint(timeslot_avail, 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) - timeslots = { - "info": {"duration": float(self.event.export_slot)}, - "blocks": [], - } - - rooms = Room.objects.filter(event=self.event) - - ak_availabilities = { - ak.pk: Availability.union(ak.availabilities.all()) - for ak in AK.objects.filter(event=self.event).all() - } - 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) - } - - blocks = list(self.event.discretize_timeslots()) - - block_names = [] - - for block_idx, block in enumerate(blocks): - current_block = [] - - if not block: - continue - - block_start = block[0].avail.start.astimezone(self.event.timezone) - block_end = block[-1].avail.end.astimezone(self.event.timezone) - - start_day = block_start.strftime("%A, %d. %b") - if block_start.date() == block_end.date(): - # same day - time_str = block_start.strftime("%H:%M") + " – " + block_end.strftime("%H:%M") - else: - # different days - time_str = block_start.strftime("%a %H:%M") + " – " + block_end.strftime("%a %H:%M") - block_names.append([start_day, time_str]) - - block_timeconstraints = [f"notblock{idx}" for idx in range(len(blocks)) if idx != block_idx] - - for timeslot in block: - time_constraints = [] - # if reso_deadline is set and timeslot ends before it, - # add fulfilled time constraint 'resolution' - if self.event.reso_deadline is None or timeslot.avail.end < self.event.reso_deadline: - time_constraints.append("resolution") - - # add fulfilled time constraints for all AKs that cannot happen during full event - time_constraints.extend( - self._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( - self._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( - self._generate_time_constraints("room", room_availabilities, timeslot.avail) - ) - - # add fulfilled time constraints for all AKSlots fixed to happen during timeslot - time_constraints.extend([ - f"fixed-akslot-{slot.id}" - for slot in AKSlot.objects.filter(event=self.event, fixed=True) - .exclude(start__isnull=True) - if self._test_akslot_fixed_in_timeslot(slot, timeslot.avail) - ]) - - time_constraints.extend(timeslot.constraints) - time_constraints.extend(block_timeconstraints) - - current_block.append({ - "id": timeslot.idx, - "info": { - "start": timeslot.avail.start.astimezone(self.event.timezone).strftime("%Y-%m-%d %H:%M"), - "end": timeslot.avail.end.astimezone(self.event.timezone).strftime("%Y-%m-%d %H:%M"), - }, - "fulfilled_time_constraints": time_constraints, - }) - - timeslots["blocks"].append(current_block) - - timeslots["info"]["blocknames"] = block_names - - 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) - - data = { - "participants": [], - "rooms": [r.as_json_dict() for r in rooms], - "timeslots": timeslots, - "info": info_dict, - "aks": [ak.as_json_dict() for ak in context["slots"]], - } + data = self.event.as_json_dict() context["json_data_oneline"] = json.dumps(data) context["json_data"] = json.dumps(data, indent=2) return context - - class AKWikiExportView(AdminViewMixin, DetailView): """ View: Export AKs of this event in wiki syntax -- GitLab From 48cb609ea03728a07527de9d42cad3cbd12fd650 Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Wed, 26 Feb 2025 16:52:36 +0100 Subject: [PATCH 71/77] Fix docstrings --- AKModel/models.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/AKModel/models.py b/AKModel/models.py index 22e0012e..a649fd0d 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -888,7 +888,7 @@ class AK(models.Model): Get a list of stringified representations of all owners :return: list of owners - :rtype: List[str] + :rtype: list[str] """ return ", ".join(str(owner) for owner in self.owners.all()) @@ -898,7 +898,7 @@ class AK(models.Model): Get a list of stringified representations of all durations of associated slots :return: list of durations - :rtype: List[str] + :rtype: list[str] """ return ", ".join(str(slot.duration_simplified) for slot in self.akslot_set.select_related('event').all()) @@ -1004,12 +1004,12 @@ class Room(models.Model): return self.title def as_json_dict(self) -> dict[str, Any]: - """Return a json string representation of this room object. + """Return a json representation of this room object. - :return: The json string representation is constructed + :return: The json dict 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 + :rtype: dict[str, Any] """ # local import to prevent cyclic import # pylint: disable=import-outside-toplevel @@ -1136,12 +1136,12 @@ class AKSlot(models.Model): super().save(force_insert, force_update, using, update_fields) def as_json_dict(self) -> dict[str, Any]: - """Return a json string representation of the AK object of this slot. + """Return a json representation of the AK object of this slot. - :return: The json string representation is constructed + :return: The json dict 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 + :rtype: dict[str, Any] """ # local import to prevent cyclic import # pylint: disable=import-outside-toplevel -- GitLab From a4ab46ffcde89236f149941d7775a8d58dc2fdcc Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Wed, 26 Feb 2025 17:32:15 +0100 Subject: [PATCH 72/77] Sort export lists to make dict eq test reliable --- AKModel/models.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/AKModel/models.py b/AKModel/models.py index a649fd0d..fc2157a3 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -536,8 +536,8 @@ class Event(models.Model): "blocks": [], } - rooms = Room.objects.filter(event=self) - slots = AKSlot.objects.filter(event=self) + rooms = Room.objects.filter(event=self).order_by() + slots = AKSlot.objects.filter(event=self).order_by() ak_availabilities = { ak.pk: Availability.union(ak.availabilities.all()) @@ -614,7 +614,7 @@ class Event(models.Model): "start": timeslot.avail.start.astimezone(self.timezone).strftime("%Y-%m-%d %H:%M"), "end": timeslot.avail.end.astimezone(self.timezone).strftime("%Y-%m-%d %H:%M"), }, - "fulfilled_time_constraints": time_constraints, + "fulfilled_time_constraints": sorted(time_constraints), }) timeslots["blocks"].append(current_block) @@ -1038,6 +1038,7 @@ class Room(models.Model): if not any(constr.startswith("proxy") for constr in data["fulfilled_room_constraints"]): data["fulfilled_room_constraints"].append("no-proxy") + data["fulfilled_room_constraints"].sort() return data @@ -1174,9 +1175,11 @@ class AKSlot(models.Model): "duration": math.ceil(self.duration / self.event.export_slot - ceil_offet_eps), "properties": { "conflicts": - [conflict.pk for conflict in conflict_slots.all()] - + [second_slot.pk for second_slot in other_ak_slots.all()], - "dependencies": [dep.pk for dep in dependency_slots.all()], + sorted( + [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()]), }, "room_constraints": [constraint.name for constraint in self.ak.requirements.all()], @@ -1206,6 +1209,9 @@ class AKSlot(models.Model): if not any(constr.startswith("proxy") for constr in data["room_constraints"]): data["room_constraints"].append("no-proxy") + data["room_constraints"].sort() + data["time_constraints"].sort() + return data class AKOrgaMessage(models.Model): -- GitLab From c2a4b946fb63280798769a68edbd446a918bf7d0 Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Wed, 26 Feb 2025 17:32:23 +0100 Subject: [PATCH 73/77] Add inconsistency check --- AKModel/models.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/AKModel/models.py b/AKModel/models.py index fc2157a3..a25e0b05 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -412,7 +412,7 @@ class Event(models.Model): yield from self.uniform_time_slots(slots_in_an_hour=slots_in_an_hour) @transaction.atomic - def schedule_from_json(self, schedule: str) -> int: + def schedule_from_json(self, schedule: str, *, check_for_data_inconsistency: bool = True) -> int: """Load AK schedule from a json string. :param schedule: A string that can be decoded to json, describing @@ -421,6 +421,10 @@ class Event(models.Model): https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format """ schedule = json.loads(schedule) + export_dict = self.as_json_dict() + + if check_for_data_inconsistency and schedule["input"] != export_dict: + raise ValueError("Data has changed since the export. Reexport and run the solver again.") slots_in_an_hour = schedule["input"]["timeslots"]["info"]["duration"] -- GitLab From da0e1a2885d4deaa07e3f69435210912634a23d4 Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Thu, 27 Feb 2025 20:28:15 +0100 Subject: [PATCH 74/77] Add AK types to json export --- AKModel/models.py | 1 + AKModel/tests/test_json_export.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/AKModel/models.py b/AKModel/models.py index c2383c33..a612f49e 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -1075,6 +1075,7 @@ class AKSlot(models.Model): "reso": self.ak.reso, "duration_in_hours": float(self.duration), "django_ak_id": self.ak.pk, + "types": list(self.ak.types.values_list("name", flat=True).order_by()), }, } diff --git a/AKModel/tests/test_json_export.py b/AKModel/tests/test_json_export.py index 8bfe554d..cbd37013 100644 --- a/AKModel/tests/test_json_export.py +++ b/AKModel/tests/test_json_export.py @@ -143,6 +143,7 @@ class JSONExportTest(TestCase): "reso", "duration_in_hours", "django_ak_id", + "types", }, f"{item} info keys not as expected", ) @@ -445,6 +446,10 @@ class JSONExportTest(TestCase): ) self.assertEqual(ak["info"]["description"], slot.ak.description) self.assertEqual(ak["info"]["django_ak_id"], slot.ak.pk) + self.assertEqual( + ak["info"]["types"], + list(slot.ak.types.values_list("name", flat=True).order_by()), + ) def test_ak_room_constraints(self): """Test if AK room constraints are exported as expected.""" -- GitLab From bb75b77162f1b8883a8ddf6301c9f36bd3970fa8 Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Fri, 28 Feb 2025 14:41:32 +0100 Subject: [PATCH 75/77] Prefetch in test and fix typing --- AKModel/availability/forms.py | 2 +- AKModel/tests/test_json_export.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/AKModel/availability/forms.py b/AKModel/availability/forms.py index 994949a8..24a7c4f0 100644 --- a/AKModel/availability/forms.py +++ b/AKModel/availability/forms.py @@ -183,7 +183,7 @@ class AvailabilitiesFormMixin(forms.Form): for avail in availabilities: setattr(avail, reference_name, instance.id) - def _replace_availabilities(self, instance, availabilities: [Availability]): + def _replace_availabilities(self, instance, availabilities: list[Availability]): """ Replace the existing list of availabilities belonging to an entity with a new, updated one diff --git a/AKModel/tests/test_json_export.py b/AKModel/tests/test_json_export.py index cbd37013..e8aac8fe 100644 --- a/AKModel/tests/test_json_export.py +++ b/AKModel/tests/test_json_export.py @@ -78,7 +78,13 @@ class JSONExportTest(TestCase): for participant in self.export_dict["participants"] } - self.ak_slots = AKSlot.objects.filter(event__slug=event.slug).all() + self.ak_slots = ( + AKSlot.objects.filter(event__slug=event.slug) + .select_related("ak") + .prefetch_related("ak__conflicts") + .prefetch_related("ak__prerequisites") + .all() + ) self.rooms = Room.objects.filter(event__slug=event.slug).all() self.slots_in_an_hour = 1 / self.export_dict["timeslots"]["info"]["duration"] self.event = event @@ -563,7 +569,7 @@ class JSONExportTest(TestCase): # test if time availability of room is restricted if not Availability.is_event_covered( - room.event, room.availabilities.all() + event, room.availabilities.all() ): time_constraints.add(f"availability-room-{room.pk}") -- GitLab From 3a211c60c59b9a7ce496df5e4f606ece80aea609 Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Sun, 2 Mar 2025 23:53:17 +0100 Subject: [PATCH 76/77] Change JSONExportView to DetailView of an Event --- AKModel/views/ak.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py index b4af5a8f..461edd3a 100644 --- a/AKModel/views/ak.py +++ b/AKModel/views/ak.py @@ -39,23 +39,20 @@ class AKCSVExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): return super().get_queryset().order_by("ak__track") -class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): +class AKJSONExportView(AdminViewMixin, DetailView): """ 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" + model = Event + context_object_name = "event" title = _("AK JSON Export") - - def get_queryset(self): - return super().get_queryset().order_by("ak__track") + slug_url_kwarg = "event_slug" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - data = self.event.as_json_dict() - + data = context["event"].as_json_dict() context["json_data_oneline"] = json.dumps(data) context["json_data"] = json.dumps(data, indent=2) -- GitLab From abe8fc86082eff8b70f543ded88ad3bbd01d5cf4 Mon Sep 17 00:00:00 2001 From: Felix Blanke <info@fblanke.de> Date: Mon, 3 Mar 2025 01:43:49 +0100 Subject: [PATCH 77/77] Update translation lines --- AKModel/locale/de_DE/LC_MESSAGES/django.po | 332 ++++++++++----------- 1 file changed, 166 insertions(+), 166 deletions(-) diff --git a/AKModel/locale/de_DE/LC_MESSAGES/django.po b/AKModel/locale/de_DE/LC_MESSAGES/django.po index 00b78ea9..38b3d54a 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-02-27 15:13+0000\n" +"POT-Creation-Date: 2025-03-03 00:42+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:720 AKModel/models.py:1167 -#: AKModel/models.py:1203 AKModel/templates/admin/AKModel/aks_by_user.html:12 +#: 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/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:257 +#: AKModel/admin.py:335 AKModel/views/ak.py:123 msgid "Reset interest in AKs" msgstr "Interesse an AKs zurücksetzen" -#: AKModel/admin.py:345 AKModel/views/ak.py:272 +#: AKModel/admin.py:345 AKModel/views/ak.py:138 msgid "Reset AKs' interest counters" msgstr "Interessenszähler der AKs zurücksetzen" @@ -126,18 +126,18 @@ msgid "Please fill in your availabilities!" msgstr "Bitte Verfügbarkeiten eintragen!" #: AKModel/availability/models.py:43 AKModel/models.py:166 -#: AKModel/models.py:499 AKModel/models.py:576 AKModel/models.py:609 -#: AKModel/models.py:635 AKModel/models.py:654 AKModel/models.py:710 -#: AKModel/models.py:868 AKModel/models.py:944 AKModel/models.py:1106 -#: AKModel/models.py:1163 AKModel/models.py:1354 +#: 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 msgid "Event" msgstr "Event" -#: AKModel/availability/models.py:44 AKModel/models.py:500 -#: AKModel/models.py:577 AKModel/models.py:610 AKModel/models.py:636 -#: AKModel/models.py:655 AKModel/models.py:711 AKModel/models.py:869 -#: AKModel/models.py:945 AKModel/models.py:1107 AKModel/models.py:1164 -#: AKModel/models.py:1355 +#: 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 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:872 -#: AKModel/models.py:934 AKModel/models.py:1173 +#: AKModel/availability/models.py:61 AKModel/models.py:1027 +#: AKModel/models.py:1090 AKModel/models.py:1335 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:719 -#: AKModel/models.py:933 AKModel/models.py:1101 +#: AKModel/availability/models.py:70 AKModel/models.py:874 +#: AKModel/models.py:1089 AKModel/models.py:1263 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:580 -#: AKModel/models.py:1179 +#: AKModel/availability/models.py:79 AKModel/models.py:735 +#: AKModel/models.py:1341 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:768 +#: AKModel/availability/models.py:309 AKModel/models.py:923 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:1348 +#: AKModel/forms.py:198 AKModel/models.py:1510 msgid "Default Slots" msgstr "Standardslots" @@ -314,9 +314,9 @@ msgstr "Aktivieren?" msgid "Finish" msgstr "Abschluss" -#: AKModel/models.py:120 AKModel/models.py:568 AKModel/models.py:606 -#: AKModel/models.py:633 AKModel/models.py:652 AKModel/models.py:670 -#: AKModel/models.py:860 +#: 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 msgid "Name" msgstr "Name" @@ -470,12 +470,12 @@ msgstr "" msgid "Events" msgstr "Events" -#: AKModel/models.py:440 +#: AKModel/models.py:444 #, 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:450 +#: AKModel/models.py:454 #, python-brace-format msgid "" "Duration of AK {ak_name} assigned by solver ({solver_duration} hours) is " @@ -485,7 +485,7 @@ msgstr "" "Stunden) ist kürzer als die aktuell vorgesehene Dauer des Slots " "({slot_duration} Stunden)" -#: AKModel/models.py:464 +#: AKModel/models.py:468 #, python-brace-format msgid "" "Fixed AK {ak_name} assigned by solver to room {solver_room} is fixed to room " @@ -494,7 +494,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:475 +#: AKModel/models.py:479 #, python-brace-format msgid "" "Fixed AK {ak_name} assigned by solver to start at {solver_start} is fixed to " @@ -503,71 +503,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:494 +#: AKModel/models.py:649 msgid "Nickname" msgstr "Spitzname" -#: AKModel/models.py:494 +#: AKModel/models.py:649 msgid "Name to identify an AK owner by" msgstr "Name, durch den eine AK-Leitung identifiziert wird" -#: AKModel/models.py:495 +#: AKModel/models.py:650 msgid "Slug" msgstr "Slug" -#: AKModel/models.py:495 +#: AKModel/models.py:650 msgid "Slug for URL generation" msgstr "Slug für URL-Generierung" -#: AKModel/models.py:496 +#: AKModel/models.py:651 msgid "Institution" msgstr "Instutution" -#: AKModel/models.py:496 +#: AKModel/models.py:651 msgid "Uni etc." msgstr "Universität o.ä." -#: AKModel/models.py:497 AKModel/models.py:679 +#: AKModel/models.py:652 AKModel/models.py:834 msgid "Web Link" msgstr "Internet Link" -#: AKModel/models.py:497 +#: AKModel/models.py:652 msgid "Link to Homepage" msgstr "Link zu Homepage oder Webseite" -#: AKModel/models.py:503 AKModel/models.py:1172 +#: AKModel/models.py:658 AKModel/models.py:1334 msgid "AK Owner" msgstr "AK-Leitung" -#: AKModel/models.py:504 +#: AKModel/models.py:659 msgid "AK Owners" msgstr "AK-Leitungen" -#: AKModel/models.py:568 +#: AKModel/models.py:723 msgid "Name of the AK Category" msgstr "Name der AK-Kategorie" -#: AKModel/models.py:569 AKModel/models.py:607 +#: AKModel/models.py:724 AKModel/models.py:762 msgid "Color" msgstr "Farbe" -#: AKModel/models.py:569 AKModel/models.py:607 +#: AKModel/models.py:724 AKModel/models.py:762 msgid "Color for displaying" msgstr "Farbe für die Anzeige" -#: AKModel/models.py:570 AKModel/models.py:673 +#: AKModel/models.py:725 AKModel/models.py:828 msgid "Description" msgstr "Beschreibung" -#: AKModel/models.py:571 +#: AKModel/models.py:726 msgid "Short description of this AK Category" msgstr "Beschreibung der AK-Kategorie" -#: AKModel/models.py:572 +#: AKModel/models.py:727 msgid "Present by default" msgstr "Defaultmäßig präsentieren" -#: AKModel/models.py:573 +#: AKModel/models.py:728 msgid "" "Present AKs of this category by default if AK owner did not specify whether " "this AK should be presented?" @@ -575,152 +575,152 @@ msgstr "" "AKs dieser Kategorie standardmäßig vorstellen, wenn die Leitungen das für " "ihren AK nicht explizit spezifiziert haben?" -#: AKModel/models.py:581 +#: AKModel/models.py:736 msgid "AK Categories" msgstr "AK-Kategorien" -#: AKModel/models.py:606 +#: AKModel/models.py:761 msgid "Name of the AK Track" msgstr "Name des AK-Tracks" -#: AKModel/models.py:613 +#: AKModel/models.py:768 msgid "AK Track" msgstr "AK-Track" -#: AKModel/models.py:614 +#: AKModel/models.py:769 msgid "AK Tracks" msgstr "AK-Tracks" -#: AKModel/models.py:633 +#: AKModel/models.py:788 msgid "Name of the Requirement" msgstr "Name der Anforderung" -#: AKModel/models.py:639 AKModel/models.py:1176 +#: AKModel/models.py:794 AKModel/models.py:1338 msgid "AK Requirement" msgstr "AK-Anforderung" -#: AKModel/models.py:640 +#: AKModel/models.py:795 msgid "AK Requirements" msgstr "AK-Anforderungen" -#: AKModel/models.py:652 +#: AKModel/models.py:807 msgid "Name describing the type" msgstr "Name, der den Typ beschreibt" -#: AKModel/models.py:658 +#: AKModel/models.py:813 msgid "AK Type" msgstr "AK Typ" -#: AKModel/models.py:659 +#: AKModel/models.py:814 msgid "AK Types" msgstr "AK-Typen" -#: AKModel/models.py:670 +#: AKModel/models.py:825 msgid "Name of the AK" msgstr "Name des AKs" -#: AKModel/models.py:671 +#: AKModel/models.py:826 msgid "Short Name" msgstr "Kurzer Name" -#: AKModel/models.py:672 +#: AKModel/models.py:827 msgid "Name displayed in the schedule" msgstr "Name zur Anzeige im AK-Plan" -#: AKModel/models.py:673 +#: AKModel/models.py:828 msgid "Description of the AK" msgstr "Beschreibung des AKs" -#: AKModel/models.py:675 +#: AKModel/models.py:830 msgid "Owners" msgstr "Leitungen" -#: AKModel/models.py:676 +#: AKModel/models.py:831 msgid "Those organizing the AK" msgstr "Menschen, die den AK organisieren und halten" -#: AKModel/models.py:679 +#: AKModel/models.py:834 msgid "Link to wiki page" msgstr "Link zur Wiki Seite" -#: AKModel/models.py:680 +#: AKModel/models.py:835 msgid "Protocol Link" msgstr "Protokolllink" -#: AKModel/models.py:680 +#: AKModel/models.py:835 msgid "Link to protocol" msgstr "Link zum Protokoll" -#: AKModel/models.py:682 +#: AKModel/models.py:837 msgid "Category" msgstr "Kategorie" -#: AKModel/models.py:683 +#: AKModel/models.py:838 msgid "Category of the AK" msgstr "Kategorie des AKs" -#: AKModel/models.py:684 AKModel/models.py:749 +#: AKModel/models.py:839 AKModel/models.py:904 msgid "Types" msgstr "Typen" -#: AKModel/models.py:685 +#: AKModel/models.py:840 msgid "This AK is" msgstr "Dieser AK ist" -#: AKModel/models.py:686 +#: AKModel/models.py:841 msgid "Track" msgstr "Track" -#: AKModel/models.py:687 +#: AKModel/models.py:842 msgid "Track the AK belongs to" msgstr "Track zu dem der AK gehört" -#: AKModel/models.py:689 +#: AKModel/models.py:844 msgid "Resolution Intention" msgstr "Resolutionsabsicht" -#: AKModel/models.py:690 +#: AKModel/models.py:845 msgid "Intends to submit a resolution" msgstr "Beabsichtigt eine Resolution einzureichen" -#: AKModel/models.py:691 +#: AKModel/models.py:846 msgid "Present this AK" msgstr "AK präsentieren" -#: AKModel/models.py:692 +#: AKModel/models.py:847 msgid "Present results of this AK" msgstr "Die Ergebnisse dieses AKs vorstellen" -#: AKModel/models.py:694 AKModel/models.py:747 AKModel/views/status.py:175 +#: AKModel/models.py:849 AKModel/models.py:902 AKModel/views/status.py:175 msgid "Requirements" msgstr "Anforderungen" -#: AKModel/models.py:695 +#: AKModel/models.py:850 msgid "AK's Requirements" msgstr "Anforderungen des AKs" -#: AKModel/models.py:697 +#: AKModel/models.py:852 msgid "Conflicting AKs" msgstr "AK-Konflikte" -#: AKModel/models.py:698 +#: AKModel/models.py:853 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:699 +#: AKModel/models.py:854 msgid "Prerequisite AKs" msgstr "Vorausgesetzte AKs" -#: AKModel/models.py:700 +#: AKModel/models.py:855 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:702 +#: AKModel/models.py:857 msgid "Organizational Notes" msgstr "Notizen zur Organisation" -#: AKModel/models.py:703 +#: AKModel/models.py:858 msgid "" "Notes to organizers. These are public. For private notes, please use the " "button for private messages on the detail page of this AK (after creation/" @@ -730,299 +730,299 @@ msgstr "" "Anmerkungen bitte den Button für Direktnachrichten verwenden (nach dem " "Anlegen/Bearbeiten)." -#: AKModel/models.py:706 AKModel/models.py:745 +#: AKModel/models.py:861 AKModel/models.py:900 msgid "Interest" msgstr "Interesse" -#: AKModel/models.py:706 +#: AKModel/models.py:861 msgid "Expected number of people" msgstr "Erwartete Personenzahl" -#: AKModel/models.py:707 +#: AKModel/models.py:862 msgid "Interest Counter" msgstr "Interessenszähler" -#: AKModel/models.py:708 +#: AKModel/models.py:863 msgid "People who have indicated interest online" msgstr "Anzahl Personen, die online Interesse bekundet haben" -#: AKModel/models.py:713 +#: AKModel/models.py:868 msgid "Export?" msgstr "Export?" -#: AKModel/models.py:714 +#: AKModel/models.py:869 msgid "Include AK in wiki export?" msgstr "AK bei Wiki-Export berücksichtigen?" -#: AKModel/models.py:764 +#: AKModel/models.py:919 msgid "Conflicts" msgstr "Konflikte" -#: AKModel/models.py:767 +#: AKModel/models.py:922 msgid "Prerequisites" msgstr "Voraussetzungen" -#: AKModel/models.py:860 +#: AKModel/models.py:1015 msgid "Name or number of the room" msgstr "Name oder Nummer des Raums" -#: AKModel/models.py:861 +#: AKModel/models.py:1016 msgid "Location" msgstr "Ort" -#: AKModel/models.py:862 +#: AKModel/models.py:1017 msgid "Name or number of the location" msgstr "Name oder Nummer des Ortes" -#: AKModel/models.py:863 +#: AKModel/models.py:1018 msgid "Capacity" msgstr "Kapazität" -#: AKModel/models.py:864 +#: AKModel/models.py:1019 msgid "Maximum number of people (-1 for unlimited)." msgstr "Maximale Personenzahl (-1 wenn unbeschränkt)." -#: AKModel/models.py:865 +#: AKModel/models.py:1020 msgid "Properties" msgstr "Eigenschaften" -#: AKModel/models.py:866 +#: AKModel/models.py:1021 msgid "AK requirements fulfilled by the room" msgstr "AK-Anforderungen, die dieser Raum erfüllt" -#: AKModel/models.py:873 AKModel/views/status.py:59 +#: AKModel/models.py:1028 AKModel/views/status.py:59 msgid "Rooms" msgstr "Räume" -#: AKModel/models.py:933 +#: AKModel/models.py:1089 msgid "AK being mapped" msgstr "AK, der zugeordnet wird" -#: AKModel/models.py:935 +#: AKModel/models.py:1091 msgid "Room the AK will take place in" msgstr "Raum in dem der AK stattfindet" -#: AKModel/models.py:936 AKModel/models.py:1351 +#: AKModel/models.py:1092 AKModel/models.py:1513 msgid "Slot Begin" msgstr "Beginn des Slots" -#: AKModel/models.py:936 AKModel/models.py:1351 +#: AKModel/models.py:1092 AKModel/models.py:1513 msgid "Time and date the slot begins" msgstr "Zeit und Datum zu der der AK beginnt" -#: AKModel/models.py:938 +#: AKModel/models.py:1094 msgid "Duration" msgstr "Dauer" -#: AKModel/models.py:939 +#: AKModel/models.py:1095 msgid "Length in hours" msgstr "Länge in Stunden" -#: AKModel/models.py:941 +#: AKModel/models.py:1097 msgid "Scheduling fixed" msgstr "Planung fix" -#: AKModel/models.py:942 +#: AKModel/models.py:1098 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:947 +#: AKModel/models.py:1103 msgid "Last update" msgstr "Letzte Aktualisierung" -#: AKModel/models.py:950 +#: AKModel/models.py:1106 msgid "AK Slot" msgstr "AK-Slot" -#: AKModel/models.py:951 AKModel/models.py:1169 AKModel/models.py:1204 +#: AKModel/models.py:1107 AKModel/models.py:1331 AKModel/models.py:1366 msgid "AK Slots" msgstr "AK-Slot" -#: AKModel/models.py:973 AKModel/models.py:982 +#: AKModel/models.py:1129 AKModel/models.py:1138 msgid "Not scheduled yet" msgstr "Noch nicht geplant" -#: AKModel/models.py:1102 +#: AKModel/models.py:1264 msgid "AK this message belongs to" msgstr "AK zu dem die Nachricht gehört" -#: AKModel/models.py:1103 +#: AKModel/models.py:1265 msgid "Message text" msgstr "Nachrichtentext" -#: AKModel/models.py:1104 +#: AKModel/models.py:1266 msgid "Message to the organizers. This is not publicly visible." msgstr "" "Nachricht an die Organisator*innen. Diese ist nicht öffentlich sichtbar." -#: AKModel/models.py:1108 +#: AKModel/models.py:1270 msgid "Resolved" msgstr "Erledigt" -#: AKModel/models.py:1109 +#: AKModel/models.py:1271 msgid "This message has been resolved (no further action needed)" msgstr "" "Diese Nachricht wurde vollständig bearbeitet (keine weiteren Aktionen " "notwendig)" -#: AKModel/models.py:1112 +#: AKModel/models.py:1274 msgid "AK Orga Message" msgstr "AK-Organachricht" -#: AKModel/models.py:1113 +#: AKModel/models.py:1275 msgid "AK Orga Messages" msgstr "AK-Organachrichten" -#: AKModel/models.py:1130 +#: AKModel/models.py:1292 msgid "Constraint Violation" msgstr "Constraintverletzung" -#: AKModel/models.py:1131 +#: AKModel/models.py:1293 msgid "Constraint Violations" msgstr "Constraintverletzungen" -#: AKModel/models.py:1138 +#: AKModel/models.py:1300 msgid "Owner has two parallel slots" msgstr "Leitung hat zwei Slots parallel" -#: AKModel/models.py:1139 +#: AKModel/models.py:1301 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:1140 +#: AKModel/models.py:1302 msgid "Room has two AK slots scheduled at the same time" msgstr "Raum hat zwei AK Slots gleichzeitig" -#: AKModel/models.py:1141 +#: AKModel/models.py:1303 msgid "Room does not satisfy the requirement of the scheduled AK" msgstr "Room erfüllt die Anforderungen des platzierten AKs nicht" -#: AKModel/models.py:1142 +#: AKModel/models.py:1304 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:1143 +#: AKModel/models.py:1305 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:1145 +#: AKModel/models.py:1307 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:1146 +#: AKModel/models.py:1308 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:1147 +#: AKModel/models.py:1309 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:1148 +#: AKModel/models.py:1310 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:1149 +#: AKModel/models.py:1311 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:1155 +#: AKModel/models.py:1317 msgid "Warning" msgstr "Warnung" -#: AKModel/models.py:1156 +#: AKModel/models.py:1318 msgid "Violation" msgstr "Verletzung" -#: AKModel/models.py:1158 +#: AKModel/models.py:1320 msgid "Type" msgstr "Art" -#: AKModel/models.py:1159 +#: AKModel/models.py:1321 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:1160 +#: AKModel/models.py:1322 msgid "Level" msgstr "Level" -#: AKModel/models.py:1161 +#: AKModel/models.py:1323 msgid "Severity level of the violation" msgstr "Schweregrad der Verletzung" -#: AKModel/models.py:1168 +#: AKModel/models.py:1330 msgid "AK(s) belonging to this constraint" msgstr "AK(s), die zu diesem Constraint gehören" -#: AKModel/models.py:1170 +#: AKModel/models.py:1332 msgid "AK Slot(s) belonging to this constraint" msgstr "AK Slot(s), die zu diesem Constraint gehören" -#: AKModel/models.py:1172 +#: AKModel/models.py:1334 msgid "AK Owner belonging to this constraint" msgstr "AK Leitung(en), die zu diesem Constraint gehören" -#: AKModel/models.py:1174 +#: AKModel/models.py:1336 msgid "Room belonging to this constraint" msgstr "Raum, der zu diesem Constraint gehört" -#: AKModel/models.py:1177 +#: AKModel/models.py:1339 msgid "AK Requirement belonging to this constraint" msgstr "AK Anforderung, die zu diesem Constraint gehört" -#: AKModel/models.py:1179 +#: AKModel/models.py:1341 msgid "AK Category belonging to this constraint" msgstr "AK Kategorie, di zu diesem Constraint gehört" -#: AKModel/models.py:1181 +#: AKModel/models.py:1343 msgid "Comment" msgstr "Kommentar" -#: AKModel/models.py:1181 +#: AKModel/models.py:1343 msgid "Comment or further details for this violation" msgstr "Kommentar oder weitere Details zu dieser Vereletzung" -#: AKModel/models.py:1184 +#: AKModel/models.py:1346 msgid "Timestamp" msgstr "Timestamp" -#: AKModel/models.py:1184 +#: AKModel/models.py:1346 msgid "Time of creation" msgstr "Zeitpunkt der ERstellung" -#: AKModel/models.py:1185 +#: AKModel/models.py:1347 msgid "Manually Resolved" msgstr "Manuell behoben" -#: AKModel/models.py:1186 +#: AKModel/models.py:1348 msgid "Mark this violation manually as resolved" msgstr "Markiere diese Verletzung manuell als behoben" -#: AKModel/models.py:1213 AKModel/templates/admin/AKModel/aks_by_user.html:22 +#: AKModel/models.py:1375 AKModel/templates/admin/AKModel/aks_by_user.html:22 #: AKModel/templates/admin/AKModel/requirements_overview.html:27 msgid "Details" msgstr "Details" -#: AKModel/models.py:1347 +#: AKModel/models.py:1509 msgid "Default Slot" msgstr "Standardslot" -#: AKModel/models.py:1352 +#: AKModel/models.py:1514 msgid "Slot End" msgstr "Ende des Slots" -#: AKModel/models.py:1352 +#: AKModel/models.py:1514 msgid "Time and date the slot ends" msgstr "Zeit und Datum zu der der Slot endet" -#: AKModel/models.py:1357 +#: AKModel/models.py:1519 msgid "Primary categories" msgstr "Primäre Kategorien" -#: AKModel/models.py:1358 +#: AKModel/models.py:1520 msgid "Categories that should be assigned to this slot primarily" msgstr "Kategorieren, die diesem Slot primär zugewiesen werden sollen" @@ -1229,47 +1229,47 @@ msgstr "Login" msgid "Register" msgstr "Registrieren" -#: AKModel/views/ak.py:21 +#: AKModel/views/ak.py:19 msgid "Requirements for Event" msgstr "Anforderungen für das Event" -#: AKModel/views/ak.py:38 +#: AKModel/views/ak.py:36 msgid "AK CSV Export" msgstr "AK-CSV-Export" -#: AKModel/views/ak.py:51 +#: AKModel/views/ak.py:49 msgid "AK JSON Export" msgstr "AK-JSON-Export" -#: AKModel/views/ak.py:206 +#: AKModel/views/ak.py:72 msgid "AK Wiki Export" msgstr "AK-Wiki-Export" -#: AKModel/views/ak.py:217 AKModel/views/manage.py:55 +#: AKModel/views/ak.py:83 AKModel/views/manage.py:55 msgid "Wishes" msgstr "Wünsche" -#: AKModel/views/ak.py:229 +#: AKModel/views/ak.py:95 msgid "Delete AK Orga Messages" msgstr "AK-Organachrichten löschen" -#: AKModel/views/ak.py:247 +#: AKModel/views/ak.py:113 msgid "AK Orga Messages successfully deleted" msgstr "AK-Organachrichten erfolgreich gelöscht" -#: AKModel/views/ak.py:259 +#: AKModel/views/ak.py:125 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:260 +#: AKModel/views/ak.py:126 msgid "Reset of interest in AKs successful." msgstr "Interesse an AKs erfolgreich zurückgesetzt." -#: AKModel/views/ak.py:274 +#: AKModel/views/ak.py:140 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:275 +#: AKModel/views/ak.py:141 msgid "AKs' interest counters set back to 0." msgstr "Interessenszähler der AKs zurückgesetzt" -- GitLab