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()