From aa8ea7fc9881af16bdfe0322da9f04e5c2fca295 Mon Sep 17 00:00:00 2001 From: Ernesto Rico Schmidt <ernesto@helsinki.at> Date: Sun, 19 Nov 2023 17:15:29 -0400 Subject: [PATCH] refactor: extract static methods from TimeSlotManager as services remove TimeSlotManager and use them as functions --- program/models.py | 39 +------------------------------- program/services.py | 55 ++++++++++++++++++++++++++++++++++++--------- program/views.py | 8 +++---- 3 files changed, 49 insertions(+), 53 deletions(-) diff --git a/program/models.py b/program/models.py index c491fa6c..3f4b3ae3 100644 --- a/program/models.py +++ b/program/models.py @@ -18,17 +18,14 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # -from datetime import datetime - from rest_framework.exceptions import ValidationError from versatileimagefield.fields import PPOIField, VersatileImageField from django.contrib.auth.models import User from django.db import models -from django.db.models import Q, QuerySet +from django.db.models import Q from django.utils import timezone from django.utils.translation import gettext_lazy as _ -from program.utils import parse_datetime from steering.settings import THUMBNAIL_SIZES @@ -375,38 +372,6 @@ class Schedule(models.Model): ordering = ("first_date", "start_time") -class TimeSlotManager(models.Manager): - @staticmethod - def instantiate(start, end, schedule): - return TimeSlot( - start=parse_datetime(start), - end=parse_datetime(end), - schedule=schedule, - ) - - @staticmethod - def get_timerange_timeslots(start_timerange: datetime, end_timerange: datetime) -> QuerySet: - """get the timeslots between start_timerange and end_timerange""" - - return TimeSlot.objects.filter( - # start before start_timerange, end after start_timerange - Q(start__lt=start_timerange, end__gt=start_timerange) - # start after/at start_timerange, end before/at end_timerange - | Q(start__gte=start_timerange, end__lte=end_timerange) - # start before end_timerange, end after/at end_timerange - | Q(start__lt=end_timerange, end__gte=end_timerange) - ) - - @staticmethod - def get_colliding_timeslots(timeslot): - return TimeSlot.objects.filter( - (Q(start__lt=timeslot.end) & Q(end__gte=timeslot.end)) - | (Q(end__gt=timeslot.start) & Q(end__lte=timeslot.end)) - | (Q(start__gte=timeslot.start) & Q(end__lte=timeslot.end)) - | (Q(start__lte=timeslot.start) & Q(end__gte=timeslot.end)) - ) - - class TimeSlot(models.Model): end = models.DateTimeField() memo = models.TextField(blank=True) @@ -417,8 +382,6 @@ class TimeSlot(models.Model): schedule = models.ForeignKey(Schedule, on_delete=models.CASCADE, related_name="timeslots") start = models.DateTimeField() - objects = TimeSlotManager() - class Meta: ordering = ("start", "end") diff --git a/program/services.py b/program/services.py index b5da2bc2..b522bdf9 100644 --- a/program/services.py +++ b/program/services.py @@ -25,6 +25,7 @@ from dateutil.rrule import rrule from rest_framework.exceptions import ValidationError from django.core.exceptions import ObjectDoesNotExist +from django.db.models import Q, QuerySet from django.utils import timezone from django.utils.translation import gettext_lazy as _ from program.models import Note, RRule, Schedule, ScheduleConflictError, Show, TimeSlot @@ -87,6 +88,16 @@ class ScheduleCreateUpdateData(TypedDict): solutions: dict[str, str] +def create_timeslot(start: str, end: str, schedule: Schedule) -> TimeSlot: + """Creates and returns an unsaved timeslot with the given `start`, `end` and `schedule`.""" + + return TimeSlot( + start=parse_datetime(start), + end=parse_datetime(end), + schedule=schedule, + ) + + def resolve_conflicts(data: ScheduleCreateUpdateData, schedule_pk: int | None, show_pk: int): """ Resolves conflicts @@ -147,7 +158,7 @@ def resolve_conflicts(data: ScheduleCreateUpdateData, schedule_pk: int | None, s # If no solution necessary: Create the projected timeslot and skip if "solution_choices" not in timeslot or len(timeslot["collisions"]) == 0: to_create.append( - TimeSlot.objects.instantiate(timeslot["start"], timeslot["end"], new_schedule), + create_timeslot(timeslot["start"], timeslot["end"], new_schedule), ) continue @@ -180,7 +191,7 @@ def resolve_conflicts(data: ScheduleCreateUpdateData, schedule_pk: int | None, s # - Create the projected timeslot # - Delete the existing collision(s) to_create.append( - TimeSlot.objects.instantiate(timeslot["start"], timeslot["end"], new_schedule), + create_timeslot(timeslot["start"], timeslot["end"], new_schedule), ) # Delete collision(s) @@ -194,14 +205,14 @@ def resolve_conflicts(data: ScheduleCreateUpdateData, schedule_pk: int | None, s # - Keep the existing timeslot # - Create projected with end of existing start to_create.append( - TimeSlot.objects.instantiate(timeslot["start"], existing["start"], new_schedule), + create_timeslot(timeslot["start"], existing["start"], new_schedule), ) if solution == "ours-end": # - Create the projected timeslot # - Change the start of the existing collision to projected end to_create.append( - TimeSlot.objects.instantiate(timeslot["start"], timeslot["end"], new_schedule), + create_timeslot(timeslot["start"], timeslot["end"], new_schedule), ) existing_ts = TimeSlot.objects.get(pk=existing["timeslot_id"]) @@ -212,14 +223,14 @@ def resolve_conflicts(data: ScheduleCreateUpdateData, schedule_pk: int | None, s # - Keep existing # - Create projected with start time of existing end to_create.append( - TimeSlot.objects.instantiate(existing["end"], timeslot["end"], new_schedule), + create_timeslot(existing["end"], timeslot["end"], new_schedule), ) if solution == "ours-start": # - Create the projected timeslot # - Change end of existing to projected start to_create.append( - TimeSlot.objects.instantiate(timeslot["start"], timeslot["end"], new_schedule), + create_timeslot(timeslot["start"], timeslot["end"], new_schedule), ) existing_ts = TimeSlot.objects.get(pk=existing["timeslot_id"]) @@ -230,11 +241,11 @@ def resolve_conflicts(data: ScheduleCreateUpdateData, schedule_pk: int | None, s # - Keep existing # - Create two projected timeslots with end of existing start and start of existing end to_create.append( - TimeSlot.objects.instantiate(timeslot["start"], existing["start"], new_schedule), + create_timeslot(timeslot["start"], existing["start"], new_schedule), ) to_create.append( - TimeSlot.objects.instantiate(existing["end"], timeslot["end"], new_schedule), + create_timeslot(existing["end"], timeslot["end"], new_schedule), ) if solution == "ours-both": @@ -243,7 +254,7 @@ def resolve_conflicts(data: ScheduleCreateUpdateData, schedule_pk: int | None, s # - Set existing end time to projected start # - Create another one with start = projected end and end = existing end to_create.append( - TimeSlot.objects.instantiate(timeslot["start"], timeslot["end"], new_schedule), + create_timeslot(timeslot["start"], timeslot["end"], new_schedule), ) existing_ts = TimeSlot.objects.get(pk=existing["timeslot_id"]) @@ -251,7 +262,7 @@ def resolve_conflicts(data: ScheduleCreateUpdateData, schedule_pk: int | None, s to_update.append(existing_ts) to_create.append( - TimeSlot.objects.instantiate(timeslot["end"], existing["end"], new_schedule), + create_timeslot(timeslot["end"], existing["end"], new_schedule), ) # If there were any errors, don't make any db changes yet @@ -591,6 +602,17 @@ def generate_timeslots(schedule: Schedule) -> list[TimeSlot]: return timeslots +def get_colliding_timeslots(timeslot: TimeSlot) -> QuerySet[TimeSlot]: + """Gets a queryset of timeslot objects colliding with the given instance.""" + + return TimeSlot.objects.filter( + (Q(start__lt=timeslot.end) & Q(end__gte=timeslot.end)) + | (Q(end__gt=timeslot.start) & Q(end__lte=timeslot.end)) + | (Q(start__gte=timeslot.start) & Q(end__lte=timeslot.end)) + | (Q(start__lte=timeslot.start) & Q(end__gte=timeslot.end)) + ) + + def generate_conflicts(timeslots: list[TimeSlot]) -> Conflicts: """ Tests a list of timeslot objects for colliding timeslots in the database @@ -725,3 +747,16 @@ def generate_conflicts(timeslots: list[TimeSlot]) -> Conflicts: conflicts["playlists"] = {} return conflicts + + +def get_timerange_timeslots(start: datetime, end: datetime) -> QuerySet[TimeSlot]: + """Gets a queryset of timeslots between the given `start` and `end` datetime.""" + + return TimeSlot.objects.filter( + # start before `start` and end after `start` + Q(start__lt=start, end__gt=start) + # start after/at `start`, end before/at `end` + | Q(start__gte=start, end__lte=end) + # start before `end`, end after/at `end` + | Q(start__lt=end, end__gte=end) + ) diff --git a/program/views.py b/program/views.py index b6555185..3b382ce1 100644 --- a/program/views.py +++ b/program/views.py @@ -79,7 +79,7 @@ from program.serializers import ( TypeSerializer, UserSerializer, ) -from program.services import resolve_conflicts +from program.services import get_timerange_timeslots, resolve_conflicts from program.utils import ( DisabledObjectPermissionCheckMixin, NestedObjectFinderMixin, @@ -146,7 +146,7 @@ def json_day_schedule(request, year=None, month=None, day=None): end = start + timedelta(hours=24) - timeslots = TimeSlot.objects.get_timerange_timeslots(start, end).select_related("schedule") + timeslots = get_timerange_timeslots(start, end).select_related("schedule") schedule = [] for ts in timeslots: @@ -199,9 +199,7 @@ def json_playout(request): include_virtual = request.GET.get("include_virtual") == "true" - timeslots = TimeSlot.objects.get_timerange_timeslots( - schedule_start, schedule_end - ).select_related("schedule") + timeslots = get_timerange_timeslots(schedule_start, schedule_end).select_related("schedule") schedule = [] -- GitLab