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] 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