From 6773f0f6d5556310b9dc40fe0720caa041134087 Mon Sep 17 00:00:00 2001
From: Konrad Mohrfeldt <konrad.mohrfeldt@farbdev.org>
Date: Fri, 12 Jul 2024 01:45:14 +0200
Subject: [PATCH] refactor: remove old program/playout endpoints

---
 program/filters.py     |  11 ---
 program/serializers.py |  51 ------------
 program/services.py    | 170 +---------------------------------------
 program/views.py       | 171 +----------------------------------------
 steering/urls.py       |   9 ---
 5 files changed, 6 insertions(+), 406 deletions(-)

diff --git a/program/filters.py b/program/filters.py
index e54eac15..55ec829e 100644
--- a/program/filters.py
+++ b/program/filters.py
@@ -280,14 +280,3 @@ class ActiveFilterSet(StaticFilterHelpTextMixin, filters.FilterSet):
         fields = [
             "is_active",
         ]
-
-
-class VirtualTimeslotFilterSet(filters.FilterSet):
-    include_virtual = filters.BooleanFilter(
-        field_name="is_virtual",
-        help_text="Include virtual timeslot entries (default: false).",
-    )
-
-    class Meta:
-        model = models.TimeSlot
-        fields = ["include_virtual"]
diff --git a/program/serializers.py b/program/serializers.py
index 015b01b7..9f052dd0 100644
--- a/program/serializers.py
+++ b/program/serializers.py
@@ -54,9 +54,6 @@ from program.models import (
 from program.typing import (
     Logo,
     MicroProgram,
-    NestedEpisode,
-    NestedSchedule,
-    NestedShow,
     ProgramFallback,
     RadioCBASettings,
     RadioImageRequirementsSettings,
@@ -1286,51 +1283,3 @@ class RadioSettingsSerializer(serializers.ModelSerializer):
             logo=logo,
             website=obj.station_website,
         )
-
-
-# done this way to get the schema annotations for datetime right
-class NestedTimeslotSerializer(serializers.Serializer):
-    end = serializers.DateTimeField()
-    id = serializers.IntegerField(allow_null=True)
-    is_virtual = serializers.BooleanField()
-    memo = serializers.CharField()
-    playlist_id = serializers.IntegerField(allow_null=True)
-    repetition_of_id = serializers.IntegerField(allow_null=True)
-    start = serializers.DateTimeField()
-
-
-class PlayoutEntrySerializer(serializers.Serializer):
-    episode = serializers.SerializerMethodField()
-    schedule = serializers.SerializerMethodField(allow_null=True)
-    show = serializers.SerializerMethodField()
-    timeslot = NestedTimeslotSerializer()
-
-    @staticmethod
-    def get_episode(obj) -> NestedEpisode:
-        pass
-
-    @staticmethod
-    def get_schedule(obj) -> NestedSchedule:
-        pass
-
-    @staticmethod
-    def get_show(obj) -> NestedShow:
-        pass
-
-
-class ProgramEntrySerializer(serializers.Serializer):
-    episode = serializers.SerializerMethodField()
-    show = serializers.SerializerMethodField()
-    timeslot = NestedTimeslotSerializer()
-
-    @staticmethod
-    def get_episode(obj) -> NestedEpisode:
-        pass
-
-    @staticmethod
-    def get_schedule(obj) -> NestedSchedule:
-        pass
-
-    @staticmethod
-    def get_show(obj) -> NestedShow:
-        pass
diff --git a/program/services.py b/program/services.py
index fd1e6406..7af891b9 100644
--- a/program/services.py
+++ b/program/services.py
@@ -19,38 +19,19 @@
 
 import copy
 from datetime import datetime, time, timedelta
-from itertools import pairwise
 
 from dateutil.relativedelta import relativedelta
 from dateutil.rrule import rrule
-from rest_framework.exceptions import NotFound, ValidationError
+from rest_framework.exceptions import ValidationError
 
 from django.conf import settings
 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,
-    RadioSettings,
-    RRule,
-    Schedule,
-    ScheduleConflictError,
-    Show,
-    TimeSlot,
-)
+from program.models import Note, RRule, Schedule, ScheduleConflictError, Show, TimeSlot
 from program.serializers import ScheduleSerializer, TimeSlotSerializer
-from program.typing import (
-    Conflicts,
-    DayScheduleEntry,
-    NestedEpisode,
-    NestedSchedule,
-    NestedShow,
-    NestedTimeslot,
-    ScheduleCreateUpdateData,
-    ScheduleData,
-    TimerangeEntry,
-)
+from program.typing import Conflicts, ScheduleCreateUpdateData, ScheduleData
 from program.utils import parse_date, parse_datetime, parse_time
 
 
@@ -706,148 +687,3 @@ def generate_conflicts(timeslots: list[TimeSlot]) -> Conflicts:
     conflicts["playlists"] = {}
 
     return conflicts
-
-
-def make_day_schedule_entry(*, timerange_entry: TimerangeEntry) -> DayScheduleEntry:
-    """returns a day schedule entry for the given timerange entry."""
-
-    episode = timerange_entry["episode"]
-    timeslot = timerange_entry["timeslot"]
-    show = timerange_entry["show"]
-
-    return DayScheduleEntry(
-        episode=NestedEpisode(
-            id=episode.get("id"),
-            title=episode["title"],
-        ),
-        timeslot=NestedTimeslot(
-            end=timeslot["end"],
-            id=timeslot.get("id"),
-            is_virtual=bool(timeslot["is_virtual"]),
-            memo=timeslot.get("memo"),
-            playlist_id=timeslot.get("playlist_id"),
-            repetition_of_id=timeslot.get("repetition_of_id"),
-            start=timeslot["start"],
-        ),
-        show=NestedShow(
-            default_playlist_id=show["default_playlist_id"],
-            id=show["id"],
-            name=show["name"],
-        ),
-    )
-
-
-def make_timerange_entry(*, timeslot: TimeSlot) -> TimerangeEntry:
-    """returns a timerange entry for the given timeslot."""
-
-    episode = timeslot.note
-    schedule = timeslot.schedule
-    show = timeslot.schedule.show
-
-    return TimerangeEntry(
-        episode=NestedEpisode(
-            id=episode.id,
-            title=episode.title,
-        ),
-        schedule=NestedSchedule(
-            default_playlist_id=schedule.default_playlist_id,
-            id=schedule.id,
-        ),
-        show=NestedShow(
-            default_playlist_id=show.default_playlist_id,
-            id=show.id,
-            name=show.name,
-        ),
-        timeslot=NestedTimeslot(
-            end=timeslot.end.strftime("%Y-%m-%dT%H:%M:%S"),
-            id=timeslot.id,
-            is_virtual=False,
-            memo=timeslot.memo,
-            playlist_id=timeslot.playlist_id,
-            repetition_of_id=timeslot.repetition_of.id if timeslot.repetition_of_id else None,
-            start=timeslot.start.strftime("%Y-%m-%dT%H:%M:%S"),
-        ),
-    )
-
-
-def make_virtual_timerange_entry(*, gap_start: datetime, gap_end: datetime) -> TimerangeEntry:
-    """returns a timerange entry to fill the gap between `gap_start` and `gap_end`."""
-
-    if radio_settings := RadioSettings.objects.first():
-        fallback_show = radio_settings.fallback_show
-        return TimerangeEntry(
-            episode=NestedEpisode(
-                id=None,
-                title=radio_settings.pools[radio_settings.fallback_default_pool],
-            ),
-            schedule=None,
-            show=NestedShow(
-                default_playlist_id=fallback_show.default_playlist_id if fallback_show else None,
-                id=fallback_show.id if fallback_show else None,
-                name=fallback_show.name if fallback_show else "",
-            ),
-            timeslot=NestedTimeslot(
-                end=gap_end.strftime("%Y-%m-%dT%H:%M:%S"),
-                id=None,
-                is_virtual=True,
-                memo="",
-                playlist_id=None,
-                repetition_of_id=None,
-                start=gap_start.strftime("%Y-%m-%dT%H:%M:%S"),
-            ),
-        )
-    else:
-        raise NotFound(
-            detail=_("Radio settings with fallbacks not found."), code="radio_settings-not_found"
-        )
-
-
-def get_timerange_timeslot_entries(
-    timerange_start: datetime, timerange_end: datetime, include_virtual: bool = False
-) -> list[TimerangeEntry]:
-    """Gets list of timerange entries between the given `timerange_start` and `timerange_end`.
-
-    Include virtual timerange entries if requested."""
-
-    timeslots = TimeSlot.objects.filter(
-        # start before `timerange_start` and end after `timerange_start`
-        Q(start__lt=timerange_start, end__gt=timerange_start)
-        # start after/at `timerange_start`, end before/at `timerange_end`
-        | Q(start__gte=timerange_start, end__lte=timerange_end)
-        # start before `timerange_end`, end after/at `timerange_end`
-        | Q(start__lt=timerange_end, end__gte=timerange_end)
-    ).select_related("schedule")
-
-    if not include_virtual:
-        return [make_timerange_entry(timeslot=timeslot) for timeslot in timeslots]
-
-    if not timeslots:
-        return []
-
-    timeslot_entries = []
-    # gap before the first timeslot
-    first_timeslot = timeslots.first()
-    if first_timeslot.start > timerange_start:
-        timeslot_entries.append(
-            make_virtual_timerange_entry(gap_start=timerange_start, gap_end=first_timeslot.start)
-        )
-
-    for index, (current, upcoming) in enumerate(pairwise(timeslots)):
-        timeslot_entries.append(make_timerange_entry(timeslot=current))
-
-        # gap between the timeslots
-        if current.end != upcoming.start:
-            timeslot_entries.append(
-                make_virtual_timerange_entry(gap_start=current.end, gap_end=upcoming.start)
-            )
-    else:
-        timeslot_entries.append(make_timerange_entry(timeslot=first_timeslot))
-
-    # gap after the last timeslot
-    last_timeslot = timeslots.last()
-    if last_timeslot.end < timerange_end:
-        timeslot_entries.append(
-            make_virtual_timerange_entry(gap_start=last_timeslot.end, gap_end=timerange_end)
-        )
-
-    return timeslot_entries
diff --git a/program/views.py b/program/views.py
index 6b447cec..d4f37165 100644
--- a/program/views.py
+++ b/program/views.py
@@ -19,11 +19,10 @@
 #
 
 import logging
-from datetime import date, datetime, time, timedelta
+from datetime import date, datetime
 from textwrap import dedent
 
 from django_filters.rest_framework import DjangoFilterBackend
-from djangorestframework_camel_case.util import camelize
 from drf_spectacular.utils import (
     OpenApiExample,
     OpenApiParameter,
@@ -75,9 +74,7 @@ from program.serializers import (
     LinkTypeSerializer,
     MusicFocusSerializer,
     NoteSerializer,
-    PlayoutEntrySerializer,
     ProfileSerializer,
-    ProgramEntrySerializer,
     RadioSettingsSerializer,
     RRuleSerializer,
     ScheduleConflictResponseSerializer,
@@ -91,174 +88,12 @@ from program.serializers import (
     TypeSerializer,
     UserSerializer,
 )
-from program.services import (
-    get_timerange_timeslot_entries,
-    make_day_schedule_entry,
-    resolve_conflicts,
-)
-from program.utils import get_values, parse_date
+from program.services import resolve_conflicts
+from program.utils import get_values
 
 logger = logging.getLogger(__name__)
 
 
-@extend_schema_view(
-    list=extend_schema(
-        summary="List schedule for a specific date.",
-        description=(
-            "Returns a list of the schedule for a specific date."
-            "Expects parameters `year` (int), `month` (int), and `day` (int) as url components."
-            "e.g. /program/2024/01/31/"
-        ),
-        examples=[
-            OpenApiExample(
-                "Full entry",
-                response_only=True,
-                value={
-                    "episode": {"id": 2, "title": ""},
-                    "show": {"defaultPlaylistId": None, "id": 2, "name": "EINS"},
-                    "timeslot": {
-                        "end": "2024-07-03T16:30:00",
-                        "id": 2,
-                        "isVirtual": False,
-                        "memo": "",
-                        "playlistId": None,
-                        "repetitionOfId": None,
-                        "start": "2024-07-03T14:00:00",
-                    },
-                },
-            ),
-            OpenApiExample(
-                "Virtual entry",
-                response_only=True,
-                value={
-                    "episode": {"id": None, "title": "Station Fallback Pool"},
-                    "show": {"defaultPlaylistId": None, "id": 1, "name": "Musikpool"},
-                    "timeslot": {
-                        "end": "2024-07-04T00:00:00",
-                        "id": None,
-                        "isVirtual": True,
-                        "memo": "",
-                        "playlistId": None,
-                        "repetitionOfId": None,
-                        "start": "2024-07-03T22:00:00",
-                    },
-                },
-            ),
-        ],
-    ),
-)
-class APIProgramViewSet(
-    mixins.ListModelMixin,
-    viewsets.GenericViewSet,
-):
-    filterset_class = filters.VirtualTimeslotFilterSet
-    queryset = TimeSlot.objects.all()
-    serializer_class = ProgramEntrySerializer
-
-    def list(self, request, year=None, month=None, day=None):
-        # datetime.combine returns a timezone naive datetime object
-        if year is None and month is None and day is None:
-            start = timezone.make_aware(datetime.combine(timezone.now(), time(0, 0)))
-        else:
-            start = timezone.make_aware(datetime.combine(date(year, month, day), time(0, 0)))
-
-        end = start + timedelta(hours=24)
-
-        include_virtual = request.GET.get("include_virtual") == "true"
-
-        schedule = [
-            make_day_schedule_entry(timerange_entry=timeslot_entry)
-            for timeslot_entry in get_timerange_timeslot_entries(start, end, include_virtual)
-        ]
-
-        return JsonResponse(camelize(schedule), safe=False)
-
-
-@extend_schema_view(
-    list=extend_schema(
-        summary="List scheduled playout.",
-        description=(
-            "Returns a list of the scheduled playout. "
-            "The schedule will include virtual timeslots to fill unscheduled gaps if requested."
-        ),
-        # TODO: move this into the serializers
-        examples=[
-            OpenApiExample(
-                "Full entry",
-                response_only=True,
-                value={
-                    "episode": {"id": 2, "title": ""},
-                    "schedule": {"defaultPlaylistId": None, "id": 1},
-                    "show": {"defaultPlaylistId": None, "id": 2, "name": "EINS"},
-                    "timeslot": {
-                        "end": "2024-07-03T16:30:00",
-                        "id": 2,
-                        "isVirtual": False,
-                        "memo": "",
-                        "playlistId": None,
-                        "repetitionOfId": None,
-                        "start": "2024-07-03T14:00:00",
-                    },
-                },
-            ),
-            OpenApiExample(
-                "Virtual entry",
-                response_only=True,
-                value={
-                    "episode": {"id": None, "title": "Station Fallback Pool"},
-                    "schedule": None,
-                    "show": {"defaultPlaylistId": None, "id": 1, "name": "Musikpool"},
-                    "timeslot": {
-                        "end": "2024-07-10T00:00:00",
-                        "id": None,
-                        "isVirtual": True,
-                        "start": "2024-07-09T22:00:00",
-                    },
-                },
-            ),
-        ],
-    )
-)
-class APIPlayoutViewSet(
-    mixins.ListModelMixin,
-    viewsets.GenericViewSet,
-):
-    filterset_class = filters.VirtualTimeslotFilterSet
-    queryset = TimeSlot.objects.all()
-    serializer_class = PlayoutEntrySerializer
-
-    def list(self, request, *args, **kwargs):
-        """
-        Return a JSON representation of the scheduled playout.
-        Called by
-        - engine (playout) to retrieve timeslots within a given timerange
-        - internal calendar to retrieve all timeslots for a week
-        """
-
-        # datetime.now and datetime.combine return timezone naive datetime objects
-        if request.GET.get("start") is None:
-            schedule_start = timezone.make_aware(datetime.combine(datetime.now(), time(0, 0)))
-        else:
-            schedule_start = timezone.make_aware(
-                datetime.combine(parse_date(request.GET.get("start")), time(0, 0))
-            )
-
-        if request.GET.get("end") is None:
-            schedule_end = schedule_start + timedelta(days=7)
-        else:
-            schedule_end = timezone.make_aware(
-                datetime.combine(
-                    parse_date(request.GET.get("end")) + timedelta(days=1), time(0, 0)
-                )
-            )
-
-        include_virtual = request.GET.get("include_virtual") == "true"
-
-        playout = get_timerange_timeslot_entries(schedule_start, schedule_end, include_virtual)
-
-        return JsonResponse(camelize(playout), safe=False)
-
-
 @extend_schema_view(
     create=extend_schema(summary="Create a new user."),
     retrieve=extend_schema(
diff --git a/steering/urls.py b/steering/urls.py
index e807f164..46cca2d4 100644
--- a/steering/urls.py
+++ b/steering/urls.py
@@ -34,9 +34,7 @@ from program.views import (
     APILinkTypeViewSet,
     APIMusicFocusViewSet,
     APINoteViewSet,
-    APIPlayoutViewSet,
     APIProfileViewSet,
-    APIProgramViewSet,
     APIRadioSettingsViewSet,
     APIRRuleViewSet,
     APIScheduleViewSet,
@@ -67,17 +65,10 @@ router.register(r"link-types", APILinkTypeViewSet, basename="link-type")
 router.register(r"rrules", APIRRuleViewSet, basename="rrule")
 router.register(r"images", APIImageViewSet, basename="image")
 router.register(r"settings", APIRadioSettingsViewSet, basename="settings")
-router.register(r"playout", APIPlayoutViewSet, basename="playout")
-router.register(r"program/week", APIPlayoutViewSet, basename="program/week")
 
 urlpatterns = [
     path("openid/", include("oidc_provider.urls", namespace="oidc_provider")),
     path("api/v1/", include(router.urls)),
-    path(
-        "api/v1/program/<int:year>/<int:month>/<int:day>/",
-        APIProgramViewSet.as_view({"get": "list"}),
-        name="program",
-    ),
     path("api/v1/schema/", SpectacularAPIView.as_view(), name="schema"),
     path(
         "api/v1/schema/swagger-ui/",
-- 
GitLab