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