13 files + 552 − 231 Inline Compare changes Side-by-side Inline Show whitespace changes Files 13 AKModel/availability/models.py +8 −0 Original line number Diff line number Diff line Loading @@ -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') Loading AKModel/forms.py +9 −0 Original line number Diff line number Diff line Loading @@ -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"), ) AKModel/locale/de_DE/LC_MESSAGES/django.po +237 −215 File changed.Preview size limit exceeded, changes collapsed. Show changes AKModel/models.py +129 −0 Original line number Diff line number Diff line import itertools import json from datetime import timedelta from django.db import models Loading Loading @@ -162,6 +163,68 @@ 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: 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)) start = self.start + time_slot_index * slot_duration return Availability(event=self, start=start, end=start + slot_duration) def schedule_from_json(self, schedule: str) -> None: schedule = json.loads(schedule) slots_in_an_hour = schedule["input"]["timeslots"]["info"]["duration"] 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 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. Loading Loading @@ -513,6 +576,31 @@ class Room(models.Model): def __str__(self): 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 if Availability.is_event_covered(self.event, self.availabilities.all()): time_constraints = [] else: time_constraints = [f"availability-room-{self.pk}"] data = { "id": self.pk, "info": { "name": self.name, }, "capacity": self.capacity, "fulfilled_room_constraints": [constraint.name for constraint in self.properties.all()], "time_constraints": time_constraints } data["fulfilled_room_constraints"].append(f"availability-room-{self.pk}") return json.dumps(data) class AKSlot(models.Model): """ An AK Mapping matches an AK to a room during a certain time. Loading Loading @@ -608,6 +696,47 @@ 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: from AKModel.availability.models import Availability # check if ak resp. owner is available for the whole event # -> no time constraint needs to be introduced if not self.fixed and Availability.is_event_covered(self.event, self.ak.availabilities.all()): ak_time_constraints = [] else: ak_time_constraints = [f"availability-ak-{self.ak.pk}"] def _owner_time_constraints(owner: AKOwner): if Availability.is_event_covered(self.event, owner.availabilities.all()): return [] else: return [f"availability-person-{owner.pk}"] 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"].extend(ak_time_constraints) 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): """ Loading AKModel/templates/admin/AKModel/ak_json_export.html 0 → 100644 +19 −0 Original line number Diff line number Diff line {% 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 %} AKModel/urls.py +7 −2 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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()), Loading AKModel/views/ak.py +110 −1 Original line number Diff line number Diff line import json from datetime import timedelta from typing import List from django.contrib import messages from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ Loading @@ -5,7 +9,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): Loading Loading @@ -37,6 +41,111 @@ 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.union(slot.ak.availabilities.all()) for slot in context["slots"] } room_availabilities = { room.pk: Availability.union(room.availabilities.all()) for room in rooms } person_availabilities = { person.pk: Availability.union(person.availabilities.all()) for person in AKOwner.objects.filter(event=self.event) } ak_fixed = { ak: 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 _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 = [] 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 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 person, availabilities in person_availabilities.items(): if _test_add_constraint(slot, availabilities): constraints.append(f"availability-person-{person}") for person, availabilities in room_availabilities.items(): if _test_add_constraint(slot, availabilities): 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 Loading AKModel/views/manage.py +11 −1 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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) AKModel/views/status.py +8 −0 Original line number Diff line number Diff line Loading @@ -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}), Loading AKPlan/locale/de_DE/LC_MESSAGES/django.po +5 −5 Original line number Diff line number Diff line Loading @@ -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" Loading Loading @@ -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" Loading @@ -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" Loading Loading @@ -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?" Loading AKPlanning/locale/de_DE/LC_MESSAGES/django.po +3 −3 Original line number Diff line number Diff line Loading @@ -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" Loading @@ -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" AKScheduling/models.py +2 −0 Original line number Diff line number Diff line Loading @@ -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(): Loading AKSubmission/locale/de_DE/LC_MESSAGES/django.po +4 −4 Original line number Diff line number Diff line Loading @@ -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" Loading @@ -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" Loading
AKModel/availability/models.py +8 −0 Original line number Diff line number Diff line Loading @@ -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') Loading
AKModel/forms.py +9 −0 Original line number Diff line number Diff line Loading @@ -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"), )
AKModel/locale/de_DE/LC_MESSAGES/django.po +237 −215 File changed.Preview size limit exceeded, changes collapsed. Show changes
AKModel/models.py +129 −0 Original line number Diff line number Diff line import itertools import json from datetime import timedelta from django.db import models Loading Loading @@ -162,6 +163,68 @@ 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: 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)) start = self.start + time_slot_index * slot_duration return Availability(event=self, start=start, end=start + slot_duration) def schedule_from_json(self, schedule: str) -> None: schedule = json.loads(schedule) slots_in_an_hour = schedule["input"]["timeslots"]["info"]["duration"] 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 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. Loading Loading @@ -513,6 +576,31 @@ class Room(models.Model): def __str__(self): 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 if Availability.is_event_covered(self.event, self.availabilities.all()): time_constraints = [] else: time_constraints = [f"availability-room-{self.pk}"] data = { "id": self.pk, "info": { "name": self.name, }, "capacity": self.capacity, "fulfilled_room_constraints": [constraint.name for constraint in self.properties.all()], "time_constraints": time_constraints } data["fulfilled_room_constraints"].append(f"availability-room-{self.pk}") return json.dumps(data) class AKSlot(models.Model): """ An AK Mapping matches an AK to a room during a certain time. Loading Loading @@ -608,6 +696,47 @@ 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: from AKModel.availability.models import Availability # check if ak resp. owner is available for the whole event # -> no time constraint needs to be introduced if not self.fixed and Availability.is_event_covered(self.event, self.ak.availabilities.all()): ak_time_constraints = [] else: ak_time_constraints = [f"availability-ak-{self.ak.pk}"] def _owner_time_constraints(owner: AKOwner): if Availability.is_event_covered(self.event, owner.availabilities.all()): return [] else: return [f"availability-person-{owner.pk}"] 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"].extend(ak_time_constraints) 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): """ Loading
AKModel/templates/admin/AKModel/ak_json_export.html 0 → 100644 +19 −0 Original line number Diff line number Diff line {% 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 %}
AKModel/urls.py +7 −2 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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()), Loading
AKModel/views/ak.py +110 −1 Original line number Diff line number Diff line import json from datetime import timedelta from typing import List from django.contrib import messages from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ Loading @@ -5,7 +9,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): Loading Loading @@ -37,6 +41,111 @@ 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.union(slot.ak.availabilities.all()) for slot in context["slots"] } room_availabilities = { room.pk: Availability.union(room.availabilities.all()) for room in rooms } person_availabilities = { person.pk: Availability.union(person.availabilities.all()) for person in AKOwner.objects.filter(event=self.event) } ak_fixed = { ak: 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 _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 = [] 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 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 person, availabilities in person_availabilities.items(): if _test_add_constraint(slot, availabilities): constraints.append(f"availability-person-{person}") for person, availabilities in room_availabilities.items(): if _test_add_constraint(slot, availabilities): 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 Loading
AKModel/views/manage.py +11 −1 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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)
AKModel/views/status.py +8 −0 Original line number Diff line number Diff line Loading @@ -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}), Loading
AKPlan/locale/de_DE/LC_MESSAGES/django.po +5 −5 Original line number Diff line number Diff line Loading @@ -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" Loading Loading @@ -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" Loading @@ -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" Loading Loading @@ -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?" Loading
AKPlanning/locale/de_DE/LC_MESSAGES/django.po +3 −3 Original line number Diff line number Diff line Loading @@ -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" Loading @@ -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"
AKScheduling/models.py +2 −0 Original line number Diff line number Diff line Loading @@ -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(): Loading
AKSubmission/locale/de_DE/LC_MESSAGES/django.po +4 −4 Original line number Diff line number Diff line Loading @@ -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" Loading @@ -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" Loading