Skip to content
Snippets Groups Projects
Commit e5fa470d authored by Felix Blanke's avatar Felix Blanke
Browse files

Adress pylint warnings

parent 77f4dac9
No related branches found
No related tags found
2 merge requests!4Draft: Add object import from JSON data,!3Merge into fork's `main` branch
Pipeline #236557 failed
...@@ -247,7 +247,14 @@ class Availability(models.Model): ...@@ -247,7 +247,14 @@ class Availability(models.Model):
f'{self.end.astimezone(self.event.timezone).strftime("%a %H:%M")}') f'{self.end.astimezone(self.event.timezone).strftime("%a %H:%M")}')
@classmethod @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. Create an availability covering exactly the time between event start and event end.
Can e.g., be used to create default availabilities. Can e.g., be used to create default availabilities.
...@@ -268,7 +275,14 @@ class Availability(models.Model): ...@@ -268,7 +275,14 @@ class Availability(models.Model):
room=room, ak=ak, ak_category=ak_category) room=room, ak=ak, ak_category=ak_category)
@classmethod @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 # NOTE: Cannot use `Availability.with_event_length` as its end is the
# event end + 1 day # event end + 1 day
full_event = Availability(event=event, start=event.start, end=event.end) full_event = Availability(event=event, start=event.start, end=event.end)
......
...@@ -275,6 +275,7 @@ class RoomFormWithAvailabilities(AvailabilitiesFormMixin, RoomForm): ...@@ -275,6 +275,7 @@ class RoomFormWithAvailabilities(AvailabilitiesFormMixin, RoomForm):
class JSONImportForm(AdminIntermediateForm): class JSONImportForm(AdminIntermediateForm):
"""Form to import an AK schedule from a json file."""
json_data = forms.CharField( json_data = forms.CharField(
required=True, required=True,
widget=forms.Textarea, widget=forms.Textarea,
......
...@@ -8,7 +8,6 @@ from django.apps import apps ...@@ -8,7 +8,6 @@ from django.apps import apps
from django.db.models import Count from django.db.models import Count
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils import timezone from django.utils import timezone
from django.utils.datetime_safe import datetime
from django.utils.text import slugify from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from simple_history.models import HistoricalRecords from simple_history.models import HistoricalRecords
...@@ -167,6 +166,24 @@ class Event(models.Model): ...@@ -167,6 +166,24 @@ class Event(models.Model):
def _generate_slots_from_block( def _generate_slots_from_block(
self, start: datetime, end: datetime, slot_duration: timedelta, slot_index: int = 0 self, start: datetime, end: datetime, slot_duration: timedelta, slot_index: int = 0
) -> Iterable[list[int, "Availability"]]: ) -> 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 from AKModel.availability.models import Availability
current_slot_start = start current_slot_start = start
...@@ -208,14 +225,32 @@ class Event(models.Model): ...@@ -208,14 +225,32 @@ class Event(models.Model):
return slot_index 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=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( yield from self._generate_slots_from_block(
start=self.start, start=self.start,
end=self.end, 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"]]: 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 slot_index = 0
for block_slot in DefaultSlot.objects.filter(event=self).order_by("start", "end"): for block_slot in DefaultSlot.objects.filter(event=self).order_by("start", "end"):
...@@ -228,6 +263,13 @@ class Event(models.Model): ...@@ -228,6 +263,13 @@ class Event(models.Model):
) )
def schedule_from_json(self, schedule: str) -> None: 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) schedule = json.loads(schedule)
slots_in_an_hour = schedule["input"]["timeslots"]["info"]["duration"] slots_in_an_hour = schedule["input"]["timeslots"]["info"]["duration"]
...@@ -602,6 +644,15 @@ class Room(models.Model): ...@@ -602,6 +644,15 @@ class Room(models.Model):
return self.title return self.title
def as_json(self) -> str: 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 from AKModel.availability.models import Availability
# check if room is available for the whole event # check if room is available for the whole event
...@@ -725,6 +776,15 @@ class AKSlot(models.Model): ...@@ -725,6 +776,15 @@ class AKSlot(models.Model):
super().save(force_insert, force_update, using, update_fields) super().save(force_insert, force_update, using, update_fields)
def as_json(self) -> str: 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 from AKModel.availability.models import Availability
# check if ak resp. owner is available for the whole event # check if ak resp. owner is available for the whole event
...@@ -738,9 +798,9 @@ class AKSlot(models.Model): ...@@ -738,9 +798,9 @@ class AKSlot(models.Model):
def _owner_time_constraints(owner: AKOwner): def _owner_time_constraints(owner: AKOwner):
if Availability.is_event_covered(self.event, owner.availabilities.all()): if Availability.is_event_covered(self.event, owner.availabilities.all()):
return [] return []
else:
return [f"availability-person-{owner.pk}"] return [f"availability-person-{owner.pk}"]
# self.slots_in_an_hour is set in AKJSONExportView
data = { data = {
"id": str(self.pk), "id": str(self.pk),
"duration": int(self.duration * self.slots_in_an_hour), "duration": int(self.duration * self.slots_in_an_hour),
......
import json import json
from datetime import timedelta
from typing import List from typing import List
from django.contrib import messages from django.contrib import messages
...@@ -7,6 +6,7 @@ from django.urls import reverse_lazy ...@@ -7,6 +6,7 @@ from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import ListView, DetailView from django.views.generic import ListView, DetailView
from AKModel.availability.models import Availability
from AKModel.metaviews.admin import AdminViewMixin, FilterByEventSlugMixin, EventSlugMixin, IntermediateAdminView, \ from AKModel.metaviews.admin import AdminViewMixin, FilterByEventSlugMixin, EventSlugMixin, IntermediateAdminView, \
IntermediateAdminActionView IntermediateAdminActionView
from AKModel.models import AKRequirement, AKSlot, Event, AKOrgaMessage, AK, Room, AKOwner from AKModel.models import AKRequirement, AKSlot, Event, AKOrgaMessage, AK, Room, AKOwner
...@@ -50,25 +50,44 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): ...@@ -50,25 +50,44 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
context_object_name = "slots" context_object_name = "slots"
title = _("AK JSON Export") 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): def get_queryset(self):
return super().get_queryset().order_by("ak__track") return super().get_queryset().order_by("ak__track")
def get_context_data(self, **kwargs): 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 SLOTS_IN_AN_HOUR = 1
rooms = Room.objects.filter(event=self.event)
participants = []
timeslots = { timeslots = {
"info": {"duration": (1.0 / SLOTS_IN_AN_HOUR), }, "info": {"duration": (1.0 / SLOTS_IN_AN_HOUR), },
"blocks": [], "blocks": [],
} }
context = super().get_context_data(**kwargs)
context["rooms"] = rooms
context["participants"] = json.dumps(participants)
for slot in context["slots"]: for slot in context["slots"]:
slot.slots_in_an_hour = SLOTS_IN_AN_HOUR slot.slots_in_an_hour = SLOTS_IN_AN_HOUR
...@@ -91,22 +110,6 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): ...@@ -91,22 +110,6 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
if (values := AKSlot.objects.select_related().filter(ak__pk=ak_id, fixed=True)).exists() 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): for block in self.event.default_time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR):
current_block = [] current_block = []
...@@ -119,17 +122,20 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): ...@@ -119,17 +122,20 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
time_constraints.extend([ time_constraints.extend([
f"availability-ak-{ak_id}" f"availability-ak-{ak_id}"
for ak_id, availabilities in ak_availabilities.items() 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([ time_constraints.extend([
f"availability-person-{person_id}" f"availability-person-{person_id}"
for person_id, availabilities in person_availabilities.items() for person_id, availabilities in person_availabilities.items()
if _test_add_constraint(slot, availabilities) if self._test_add_constraint(slot, availabilities)
]) ])
time_constraints.extend([ time_constraints.extend([
f"availability-room-{room_id}" f"availability-room-{room_id}"
for room_id, availabilities in room_availabilities.items() for room_id, availabilities in room_availabilities.items()
if _test_add_constraint(slot, availabilities) if self._test_add_constraint(slot, availabilities)
]) ])
current_block.append({ current_block.append({
......
...@@ -60,7 +60,7 @@ class ExportSlidesView(EventSlugMixin, IntermediateAdminView): ...@@ -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) 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) 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 # 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) # be presented when restriction setting was chosen)
...@@ -250,6 +250,9 @@ class AKsByUserView(AdminViewMixin, EventSlugMixin, DetailView): ...@@ -250,6 +250,9 @@ class AKsByUserView(AdminViewMixin, EventSlugMixin, DetailView):
class AKJSONImportView(EventSlugMixin, IntermediateAdminView): class AKJSONImportView(EventSlugMixin, IntermediateAdminView):
"""
View: Import an AK schedule from a json file that can be pasted into this view.
"""
form_class = JSONImportForm form_class = JSONImportForm
title = _("AK JSON Import") title = _("AK JSON Import")
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment