Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • main
  • feature/export-filtering
  • feature/clear-schedule-button
  • fix/responsive-cols-in-polls
  • feature/preference-polling-form
  • feature/json-export-via-rest-framework
  • feature/json-schedule-import-tests
  • fix/add-room-import-only-once
  • ak-import
  • renovate/django-simple-history-3.x
  • renovate/django-debug-toolbar-4.x
  • renovate/django-5.x
  • renovate/mysqlclient-2.x
13 results

Target

Select target project
No results found
Select Git revision
  • renovate/beautifulsoup4-4.x
  • renovate/django-5.x
  • renovate/django-tex-1.x
  • renovate/jsonschema-4.x
  • renovate/djangorestframework-3.x
  • renovate/django-bootstrap5-25.x
  • renovate/django-debug-toolbar-6.x
  • main
  • koma/feature/preference-polling-form
9 results
Show changes

Commits on Source 14

13 files
+ 552
231
Compare changes
  • Side-by-side
  • Inline

Files

Original line number Diff line number Diff line
@@ -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')
Original line number Diff line number Diff line
@@ -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"),
    )
+129 −0
Original line number Diff line number Diff line
import itertools
import json
from datetime import timedelta

from django.db import models
@@ -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.
@@ -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.
@@ -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):
    """
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 %}
+7 −2
Original line number Diff line number Diff line
@@ -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()),
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 _
@@ -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):
@@ -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
Original line number Diff line number Diff line
@@ -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)
Original line number Diff line number Diff line
@@ -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}),
Original line number Diff line number Diff line
@@ -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?"

Original line number Diff line number Diff line
@@ -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"
Original line number Diff line number Diff line
@@ -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():
Original line number Diff line number Diff line
@@ -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"