Skip to content
Snippets Groups Projects
Verified Commit 759e2bac authored by Ernesto Rico Schmidt's avatar Ernesto Rico Schmidt
Browse files

feat: generate virtual timeslots for unscheduled calendar areas

parent aa8b5b10
No related branches found
No related tags found
No related merge requests found
Pipeline #8218 passed
......@@ -19,7 +19,8 @@
import copy
from datetime import datetime, time, timedelta
from typing import TypedDict
from itertools import pairwise
from typing import Literal, TypedDict
from dateutil.relativedelta import relativedelta
from dateutil.rrule import rrule
......@@ -30,7 +31,15 @@ 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
from program.models import (
Note,
RadioSettings,
RRule,
Schedule,
ScheduleConflictError,
Show,
TimeSlot,
)
from program.serializers import ScheduleSerializer, TimeSlotSerializer
from program.utils import parse_date, parse_datetime, parse_time
......@@ -86,6 +95,36 @@ class ScheduleCreateUpdateData(TypedDict):
solutions: dict[str, str]
class ScheduleEntry(TypedDict):
end: str
is_virtual: bool
show_id: int
start: str
title: str
class TimeslotEntry(TypedDict):
end: str
id: int
is_virtual: Literal[False]
playlist_id: int | None
repetition_of_id: int | None
schedule_default_playlist_id: int | None
schedule_id: int
show_default_playlist_id: int | None
show_id: int
start: str
title: str
class VirtualTimeslotEntry(TypedDict):
end: str
is_virtual: Literal[True]
show_id: int
start: str
title: str
def create_timeslot(start: str, end: str, schedule: Schedule) -> TimeSlot:
"""Creates and returns an unsaved timeslot with the given `start`, `end` and `schedule`."""
......@@ -740,14 +779,93 @@ def generate_conflicts(timeslots: list[TimeSlot]) -> Conflicts:
return conflicts
def get_timerange_timeslots(start: datetime, end: datetime) -> QuerySet[TimeSlot]:
"""Gets a queryset of timeslots between the given `start` and `end` datetime."""
def make_schedule_entry(*, timeslot_entry: TimeslotEntry) -> ScheduleEntry:
"""returns a schedule entry for the given timeslot entry."""
return {
"end": timeslot_entry["end"],
"show_id": timeslot_entry["show_id"],
"is_virtual": timeslot_entry["is_virtual"],
"start": timeslot_entry["start"],
"title": timeslot_entry["title"],
}
def make_timeslot_entry(*, timeslot: TimeSlot) -> TimeslotEntry:
"""returns a timeslot entry for the given timeslot."""
schedule = timeslot.schedule
show = timeslot.schedule.show
return {
"end": timeslot.end.strftime("%Y-%m-%dT%H:%M:%S %z"),
"id": timeslot.id,
"is_virtual": False,
"playlist_id": timeslot.playlist_id,
# 'timeslot.repetition_of` is a foreign key that can be null
"repetition_of_id": timeslot.repetition_of.id if timeslot.repetition_of else None,
"schedule_default_playlist_id": schedule.default_playlist_id,
"schedule_id": schedule.id,
"show_default_playlist_id": show.default_playlist_id,
"show_id": show.id,
"start": timeslot.start.strftime("%Y-%m-%dT%H:%M:%S %z"),
"title": f"{show.name} {_('REP')}" if schedule.is_repetition else show.name,
}
def make_virtual_timeslot_entry(*, gap_start: datetime, gap_end: datetime) -> VirtualTimeslotEntry:
"""returns a virtual timeslot entry to fill the gap in between `gap_start` and `gap_end`."""
return {
"end": gap_end.strftime("%Y-%m-%dT%H:%M:%S %z"),
"is_virtual": True,
"show_id": RadioSettings.objects.first().fallback_show.id,
"start": gap_start.strftime("%Y-%m-%dT%H:%M:%S %z"),
"title": RadioSettings.objects.first().fallback_default_pool,
}
def get_timerange_timeslot_entries(
timerange_start: datetime, timerange_end: datetime, include_virtual: bool = False
) -> list[TimeslotEntry | VirtualTimeslotEntry]:
"""Gets list of timeslot entries between the given `timerange_start` and `timerange_end`.
Include virtual timeslots 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_timeslot_entry(timeslot=timeslot) for timeslot in timeslots]
timeslot_entries = []
# gap before the first timeslot
first_timeslot = timeslots.first()
if first_timeslot.start > timerange_start:
timeslot_entries.append(
make_virtual_timeslot_entry(gap_start=timerange_start, gap_end=first_timeslot.start)
)
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)
)
for index, (current, upcoming) in enumerate(pairwise(timeslots)):
timeslot_entries.append(make_timeslot_entry(timeslot=current))
# gap between the timeslots
if current.end != upcoming.start:
timeslot_entries.append(
make_virtual_timeslot_entry(gap_start=current.end, gap_end=upcoming.start)
)
# gap after the last timeslot
last_timeslot = timeslots.last()
if last_timeslot.end < timerange_end:
timeslot_entries.append(
make_virtual_timeslot_entry(gap_start=last_timeslot.end, gap_end=timerange_end)
)
return timeslot_entries
......@@ -20,7 +20,6 @@
import logging
from datetime import date, datetime, time, timedelta
from itertools import pairwise
from textwrap import dedent
from django_filters.rest_framework import DjangoFilterBackend
......@@ -43,7 +42,6 @@ from django.db import IntegrityError
from django.http import HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404
from django.utils import timezone
from django.utils.translation import gettext as _
from program import filters
from program.models import (
Category,
......@@ -89,59 +87,16 @@ from program.serializers import (
TypeSerializer,
UserSerializer,
)
from program.services import get_timerange_timeslots, resolve_conflicts
from program.services import (
get_timerange_timeslot_entries,
make_schedule_entry,
resolve_conflicts,
)
from program.utils import get_values, parse_date
logger = logging.getLogger(__name__)
def timeslot_entry(*, timeslot: TimeSlot) -> dict:
"""return a timeslot entry as a dict"""
schedule = timeslot.schedule
show = timeslot.schedule.show
playlist_id = timeslot.playlist_id
title = show_name = f"{show.name} {_('REP')}" if schedule.is_repetition else show.name
# we start and end as timezone naive datetime objects
start = timezone.make_naive(timeslot.start).strftime("%Y-%m-%dT%H:%M:%S")
end = timezone.make_naive(timeslot.end).strftime("%Y-%m-%dT%H:%M:%S")
return {
"end": end,
"id": timeslot.id,
"playlistId": playlist_id,
# `Timeslot.repetition_of` is a foreign key that can be null
"repetitionOfId": timeslot.repetition_of.id if timeslot.repetition_of else None,
"scheduleDefaultPlaylistId": schedule.default_playlist_id,
"scheduleId": schedule.id,
"showCategories": ", ".join(show.category.values_list("name", flat=True)),
"showDefaultPlaylistId": show.default_playlist_id,
# `Show.funding_category` is a foreign key can be null
"showFundingCategory": show.funding_category.name if show.funding_category_id else "",
"showHosts": ", ".join(show.hosts.values_list("name", flat=True)),
"showId": show.id,
"showLanguages": ", ".join(show.language.values_list("name", flat=True)),
"showMusicFocus": ", ".join(show.music_focus.values_list("name", flat=True)),
"showName": show_name,
"showTopics": ", ".join(show.topic.values_list("name", flat=True)),
# `Show.type` is a foreign key that can be null
"showType": show.type.name if show.type_id else "",
"start": start,
"title": title,
}
def gap_entry(*, gap_start: datetime, gap_end: datetime) -> dict:
"""return a virtual timeslot to fill the gap in between `gap_start` and `gap_end` as a dict"""
return {
"end": gap_end.strftime("%Y-%m-%dT%H:%M:%S"),
"start": gap_start.strftime("%Y-%m-%dT%H:%M:%S"),
"virtual": True,
}
@extend_schema_view(
list=extend_schema(
summary="List schedule for a specific date.",
......@@ -168,18 +123,12 @@ class APIDayScheduleViewSet(
end = start + timedelta(hours=24)
timeslots = get_timerange_timeslots(start, end).select_related("schedule")
schedule = []
for ts in timeslots:
entry = {
"start": ts.start.strftime("%Y-%m-%d_%H:%M:%S"),
"end": ts.end.strftime("%Y-%m-%d_%H:%M:%S"),
"title": ts.schedule.show.name,
"id": ts.schedule.show.id,
}
include_virtual = request.GET.get("include_virtual") == "true"
schedule.append(entry)
schedule = [
make_schedule_entry(timeslot_entry=timeslot_entry)
for timeslot_entry in get_timerange_timeslot_entries(start, end, include_virtual)
]
return JsonResponse(schedule, safe=False)
......@@ -229,33 +178,9 @@ class APIPlayoutViewSet(
include_virtual = request.GET.get("include_virtual") == "true"
timeslots = get_timerange_timeslots(schedule_start, schedule_end).select_related(
"schedule"
)
schedule = []
first_timeslot = timeslots.first()
playout = get_timerange_timeslot_entries(schedule_start, schedule_end, include_virtual)
if include_virtual and first_timeslot.start > schedule_start:
schedule.append(gap_entry(gap_start=schedule_start, gap_end=first_timeslot.start))
for current, upcoming in pairwise(timeslots):
schedule.append(timeslot_entry(timeslot=current))
if include_virtual and current.end != upcoming.start:
schedule.append(gap_entry(gap_start=current.end, gap_end=upcoming.start))
last_timeslot = timeslots.last()
# we need to append the last timeslot to the schedule to complete it
if last_timeslot:
schedule.append(timeslot_entry(timeslot=last_timeslot))
if include_virtual and last_timeslot.end < schedule_end:
schedule.append(gap_entry(gap_start=last_timeslot.end, gap_end=schedule_end))
return JsonResponse(schedule, safe=False)
return JsonResponse(playout, safe=False)
@extend_schema_view(
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment