From 209082808f428e823e774641d627113f86fb7197 Mon Sep 17 00:00:00 2001 From: Ernesto Rico Schmidt <ernesto@helsinki.at> Date: Tue, 21 Feb 2023 18:57:26 -0400 Subject: [PATCH] Generate virtual timeslots for unscheduled areas - add `includeVirtual` as GET parameter for `json_playout()` - refactor `json_playout` to be cleaner Closes: #120 --- program/views.py | 139 ++++++++++++++++++++++++++++------------------- 1 file changed, 83 insertions(+), 56 deletions(-) diff --git a/program/views.py b/program/views.py index 90630e82..5c65f97f 100644 --- a/program/views.py +++ b/program/views.py @@ -21,6 +21,7 @@ import json import logging from datetime import date, datetime, time, timedelta +from itertools import pairwise from textwrap import dedent from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_view @@ -77,6 +78,54 @@ from program.utils import ( logger = logging.getLogger(__name__) +def timeslot_entry(*, timeslot: TimeSlot) -> dict: + """return a timeslot entry as a dict""" + + show = timeslot.show + schedule = timeslot.schedule + playlist_id = timeslot.playlist_id + + title = show_name = f"{show.name} {_('REP')}" if schedule.is_repetition else show.name + + return { + "id": timeslot.id, + "start": timeslot.start.strftime("%Y-%m-%dT%H:%M:%S"), + "end": timeslot.end.strftime("%Y-%m-%dT%H:%M:%S"), + "title": title, + "schedule_id": schedule.id, + "is_repetition": timeslot.is_repetition, + "playlist_id": playlist_id, + "schedule_default_playlist_id": schedule.default_playlist_id, + "show_default_playlist_id": show.default_playlist_id, + "show_id": show.id, + "show_name": show_name, + "show_hosts": ", ".join(show.hosts.values_list("name", flat=True)), + # `Show.type` is a foreign key that can be null + "show_type": show.type.name if show.type_id else "", + "show_categories": ", ".join(show.category.values_list("name", flat=True)), + "show_topics": ", ".join(show.topic.values_list("name", flat=True)), + # TODO: replace `show_musicfocus` with `show_music_focus` when engine is updated + "show_musicfocus": ", ".join(show.music_focus.values_list("name", flat=True)), + "show_languages": ", ".join(show.language.values_list("name", flat=True)), + # TODO: replace `show_fundingcategory` with `show_funding_category` when engine is + # updated + # `Show.funding_category` is a foreign key can be null + "show_fundingcategory": show.funding_category.name if show.funding_category_id else "", + "memo": timeslot.memo, + "className": "danger" if playlist_id is None or playlist_id == 0 else "default", + } + + +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 { + "start": gap_start.strftime("%Y-%m-%dT%H:%M:%S"), + "end": gap_end.strftime("%Y-%m-%dT%H:%M:%S"), + "virtual": True, + } + + def json_day_schedule(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: @@ -111,87 +160,65 @@ def json_day_schedule(request, year=None, month=None, day=None): def json_playout(request): """ - Called by - - engine (playout) to retrieve timeslots within a given timerange - Expects GET variables 'start' (date) and 'end' (date). - If start not given, it will be today + Return a JSON representation of the scheduled playout. + + Expects GET parameters `start` (date), `end` (date), and `includeVirtual` (boolean). + + - `start` is today by default. + - `end` is one week after the start date by default. + - `includeVirtual` is false by default. + + The schedule will include virtual timeslots to fill unscheduled gaps if requested. - - internal calendar to retrieve all timeslots for a week - Expects GET variable 'start' (date), otherwise start will be today - If end not given, it returns all timeslots of the next 7 days + Called by + - engine (playout) to retrieve timeslots within a given timerange + - internal calendar to retrieve all timeslots for a week """ # datetime.combine returns a timezone naive datetime object if request.GET.get("start") is None: - start = timezone.make_aware(datetime.combine(timezone.now(), time(0, 0))) + schedule_start = timezone.make_aware(datetime.combine(timezone.now(), time(0, 0))) else: - start = timezone.make_aware( + schedule_start = timezone.make_aware( datetime.combine(parse_date(request.GET.get("start")), time(0, 0)) ) if request.GET.get("end") is None: - end = start + timedelta(days=7) + schedule_end = schedule_start + timedelta(days=7) else: - end = timezone.make_aware( - datetime.combine(parse_date(request.GET.get("end")), time(23, 59)) + schedule_end = timezone.make_aware( + datetime.combine(parse_date(request.GET.get("end")) + timedelta(days=1), time(0, 0)), + timezone=timezone.get_current_timezone(), ) + include_virtual = request.GET.get("includeVirtual") == "true" + timeslots = ( - TimeSlot.objects.get_timerange_timeslots(start, end) + TimeSlot.objects.get_timerange_timeslots(schedule_start, schedule_end) .select_related("schedule") .select_related("show") ) schedule = [] - for ts in timeslots: - is_repetition = " " + _("REP") if ts.schedule.is_repetition is True else "" - - hosts = ", ".join(ts.show.hosts.values_list("name", flat=True)) - categories = ", ".join(ts.show.category.values_list("name", flat=True)) - topics = ", ".join(ts.show.topic.values_list("name", flat=True)) - music_focus = ", ".join(ts.show.music_focus.values_list("name", flat=True)) - languages = ", ".join(ts.show.language.values_list("name", flat=True)) - funding_category = ( - FundingCategory.objects.get(pk=ts.show.funding_category_id) - if ts.show.funding_category_id - else None - ) + first_timeslot = timeslots.first() - type_ = Type.objects.get(pk=ts.show.type_id) + if include_virtual and first_timeslot.start > schedule_start: + schedule.append(gap_entry(gap_start=schedule_start, gap_end=first_timeslot.start)) - classname = "default" + for current, upcoming in pairwise(timeslots): + schedule.append(timeslot_entry(timeslot=current)) - if ts.playlist_id is None or ts.playlist_id == 0: - classname = "danger" + if include_virtual and current.end != upcoming.start: + schedule.append(gap_entry(gap_start=current.end, gap_end=upcoming.start)) - entry = { - "id": ts.id, - "start": ts.start.strftime("%Y-%m-%dT%H:%M:%S"), - "end": ts.end.strftime("%Y-%m-%dT%H:%M:%S"), - "title": ts.show.name + is_repetition, # For JS Calendar - "schedule_id": ts.schedule.id, - "is_repetition": ts.is_repetition, - "playlist_id": ts.playlist_id, - "schedule_default_playlist_id": ts.schedule.default_playlist_id, - "show_default_playlist_id": ts.show.default_playlist_id, - "show_id": ts.show.id, - "show_name": ts.show.name + is_repetition, - "show_hosts": hosts, - "show_type": type_.name, - "show_categories": categories, - "show_topics": topics, - # TODO: replace `show_musicfocus` with `show_music_focus` when engine is updated - "show_musicfocus": music_focus, - "show_languages": languages, - # TODO: replace `show_fundingcategory` with `show_funding_category` when engine is - # updated - "show_fundingcategory": funding_category.name, - "memo": ts.memo, - "className": classname, - } + last_timeslot = timeslots.last() - schedule.append(entry) + # we need to append the last timeslot to the schedule to complete it + 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 HttpResponse( json.dumps(schedule, ensure_ascii=False).encode("utf8"), -- GitLab