218 lines
7.0 KiB
Python
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()
|