forked from rc/aircox
#93 Co-authored-by: bkfox <thomas bkfox net> Reviewed-on: rc/aircox#95
This commit is contained in:
217
aircox/models/schedule.py
Normal file
217
aircox/models/schedule.py
Normal file
@ -0,0 +1,217 @@
|
||||
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()
|
Reference in New Issue
Block a user