aircox/aircox/models/schedule.py
Thomas Kairos cd19c26e82 #93: reorganise Rerun, Diffusion, Schedule module (#95)
#93

Co-authored-by: bkfox <thomas bkfox net>
Reviewed-on: #95
2023-04-02 20:37:47 +02:00

218 lines
7.0 KiB
Python

import calendar
import pytz
from django.db import models
from django.utils import timezone as tz
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from aircox import utils
from .rerun import Rerun
__all__ = ("Schedule",)
# ? BIG FIXME: self.date is still used as datetime
class Schedule(Rerun):
"""A Schedule defines time slots of programs' diffusions.
It can be an initial run or a rerun (in such case it is linked to
the related schedule).
"""
# Frequency for schedules. Basically, it is a mask of bits where each bit
# is a week. Bits > rank 5 are used for special schedules.
# Important: the first week is always the first week where the weekday of
# the schedule is present.
# For ponctual programs, there is no need for a schedule, only a diffusion
class Frequency(models.IntegerChoices):
ponctual = 0b000000, _("ponctual")
first = 0b000001, _("1st {day} of the month")
second = 0b000010, _("2nd {day} of the month")
third = 0b000100, _("3rd {day} of the month")
fourth = 0b001000, _("4th {day} of the month")
last = 0b010000, _("last {day} of the month")
first_and_third = 0b000101, _("1st and 3rd {day} of the month")
second_and_fourth = 0b001010, _("2nd and 4th {day} of the month")
every = 0b011111, _("{day}")
one_on_two = 0b100000, _("one {day} on two")
date = models.DateField(
_("date"),
help_text=_("date of the first diffusion"),
)
time = models.TimeField(
_("time"),
help_text=_("start time"),
)
timezone = models.CharField(
_("timezone"),
default=lambda: tz.get_current_timezone().zone,
max_length=100,
choices=[(x, x) for x in pytz.all_timezones],
help_text=_("timezone used for the date"),
)
duration = models.TimeField(
_("duration"),
help_text=_("regular duration"),
)
frequency = models.SmallIntegerField(
_("frequency"),
choices=Frequency.choices,
)
class Meta:
verbose_name = _("Schedule")
verbose_name_plural = _("Schedules")
def __str__(self):
return "{} - {}, {}".format(
self.program.title,
self.get_frequency_display(),
self.time.strftime("%H:%M"),
)
def save_rerun(self):
super().save_rerun()
self.duration = self.initial.duration
self.frequency = self.initial.frequency
@cached_property
def tz(self):
"""Pytz timezone of the schedule."""
import pytz
return pytz.timezone(self.timezone)
@cached_property
def start(self):
"""Datetime of the start (timezone unaware)"""
return tz.datetime.combine(self.date, self.time)
@cached_property
def end(self):
"""Datetime of the end."""
return self.start + utils.to_timedelta(self.duration)
def get_frequency_display(self):
"""Return frequency formated for display."""
from django.template.defaultfilters import date
return (
self._get_FIELD_display(self._meta.get_field("frequency"))
.format(day=date(self.date, "l"))
.capitalize()
)
def normalize(self, date):
"""Return a datetime set to schedule's time for the provided date,
handling timezone (based on schedule's timezone)."""
date = tz.datetime.combine(date, self.time)
return self.tz.normalize(self.tz.localize(date))
def dates_of_month(self, date):
"""Return normalized diffusion dates of provided date's month."""
if self.frequency == Schedule.Frequency.ponctual:
return []
sched_wday, freq = self.date.weekday(), self.frequency
date = date.replace(day=1)
# last of the month
if freq == Schedule.Frequency.last:
date = date.replace(
day=calendar.monthrange(date.year, date.month)[1]
)
date_wday = date.weekday()
# end of month before the wanted weekday: move one week back
if date_wday < sched_wday:
date -= tz.timedelta(days=7)
date += tz.timedelta(days=sched_wday - date_wday)
return [self.normalize(date)]
# move to the first day of the month that matches the schedule's
# weekday. Check on SO#3284452 for the formula
date_wday, month = date.weekday(), date.month
date += tz.timedelta(
days=(7 if date_wday > sched_wday else 0) - date_wday + sched_wday
)
if freq == Schedule.Frequency.one_on_two:
# - adjust date with modulo 14 (= 2 weeks in days)
# - there are max 3 "weeks on two" per month
if (date - self.date).days % 14:
date += tz.timedelta(days=7)
dates = (date + tz.timedelta(days=14 * i) for i in range(0, 3))
else:
dates = (
date + tz.timedelta(days=7 * week)
for week in range(0, 5)
if freq & (0b1 << week)
)
return [self.normalize(date) for date in dates if date.month == month]
def diffusions_of_month(self, date):
"""Get episodes and diffusions for month of provided date, including
reruns.
:returns: tuple([Episode], [Diffusion])
"""
from .diffusion import Diffusion
from .episode import Episode
if (
self.initial is not None
or self.frequency == Schedule.Frequency.ponctual
):
return [], []
# dates for self and reruns as (date, initial)
reruns = [
(rerun, rerun.date - self.date) for rerun in self.rerun_set.all()
]
dates = {date: None for date in self.dates_of_month(date)}
dates.update(
(rerun.normalize(date.date() + delta), date)
for date in list(dates.keys())
for rerun, delta in reruns
)
# remove dates corresponding to existing diffusions
saved = set(
Diffusion.objects.filter(
start__in=dates.keys(), program=self.program, schedule=self
).values_list("start", flat=True)
)
# make diffs
duration = utils.to_timedelta(self.duration)
diffusions = {}
episodes = {}
for date, initial in dates.items():
if date in saved:
continue
if initial is None:
episode = Episode.from_page(self.program, date=date)
episode.date = date
episodes[date] = episode
else:
episode = episodes[initial]
initial = diffusions[initial]
diffusions[date] = Diffusion(
episode=episode,
schedule=self,
type=Diffusion.TYPE_ON_AIR,
initial=initial,
start=date,
end=date + duration,
)
return episodes.values(), diffusions.values()