import datetime from django.db import models from django.db.models import Q 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 .episode import Episode from .schedule import Schedule from .rerun import Rerun, RerunQuerySet __all__ = ("Diffusion", "DiffusionQuerySet") class DiffusionQuerySet(RerunQuerySet): def episode(self, episode=None, id=None): """Diffusions for this episode.""" return ( self.filter(episode=episode) if id is None else self.filter(episode__id=id) ) def on_air(self): """On air diffusions.""" return self.filter(type=Diffusion.TYPE_ON_AIR) # TODO: rename to `datetime` def now(self, now=None, order=True): """Diffusions occuring now.""" now = now or tz.now() qs = self.filter(start__lte=now, end__gte=now).distinct() return qs.order_by("start") if order else qs def date(self, date=None, order=True): """Diffusions occuring date.""" date = date or datetime.date.today() start = tz.make_aware(tz.datetime.combine(date, datetime.time())) end = tz.make_aware( tz.datetime.combine(date, datetime.time(23, 59, 59, 999)) ) # start = tz.get_current_timezone().localize(start) # end = tz.get_current_timezone().localize(end) qs = self.filter(start__range=(start, end)) return qs.order_by("start") if order else qs def at(self, date, order=True): """Return diffusions at specified date or datetime.""" return ( self.now(date, order) if isinstance(date, tz.datetime) else self.date(date, order) ) def after(self, date=None): """Return a queryset of diffusions that happen after the given date (default: today).""" date = utils.date_or_default(date) if isinstance(date, tz.datetime): qs = self.filter(Q(start__gte=date) | Q(end__gte=date)) else: qs = self.filter(Q(start__date__gte=date) | Q(end__date__gte=date)) return qs.order_by("start") def before(self, date=None): """Return a queryset of diffusions that finish before the given date (default: today).""" date = utils.date_or_default(date) if isinstance(date, tz.datetime): qs = self.filter(start__lt=date) else: qs = self.filter(start__date__lt=date) return qs.order_by("start") def range(self, start, end): # FIXME can return dates that are out of range... return self.after(start).before(end) class Diffusion(Rerun): """A Diffusion is an occurrence of a Program that is scheduled on the station's timetable. It can be a rerun of a previous diffusion. In such a case, use rerun's info instead of its own. A Diffusion without any rerun is named Episode (previously, a Diffusion was different from an Episode, but in the end, an episode only has a name, a linked program, and a list of sounds, so we finally merge theme). A Diffusion can have different types: - default: simple diffusion that is planified / did occurred - unconfirmed: a generated diffusion that has not been confirmed and thus is not yet planified - cancel: the diffusion has been canceled - stop: the diffusion has been manually stopped """ objects = DiffusionQuerySet.as_manager() TYPE_ON_AIR = 0x00 TYPE_UNCONFIRMED = 0x01 TYPE_CANCEL = 0x02 TYPE_CHOICES = ( (TYPE_ON_AIR, _("on air")), (TYPE_UNCONFIRMED, _("not confirmed")), (TYPE_CANCEL, _("cancelled")), ) episode = models.ForeignKey( Episode, models.CASCADE, verbose_name=_("episode"), ) schedule = models.ForeignKey( Schedule, models.CASCADE, verbose_name=_("schedule"), blank=True, null=True, ) type = models.SmallIntegerField( verbose_name=_("type"), default=TYPE_ON_AIR, choices=TYPE_CHOICES, ) start = models.DateTimeField(_("start"), db_index=True) end = models.DateTimeField(_("end"), db_index=True) # port = models.ForeignKey( # 'self', # verbose_name = _('port'), # blank = True, null = True, # on_delete=models.SET_NULL, # help_text = _('use this input port'), # ) item_template_name = "aircox/widgets/diffusion_item.html" class Meta: verbose_name = _("Diffusion") verbose_name_plural = _("Diffusions") permissions = ( ("programming", _("edit the diffusions' planification")), ) def __str__(self): str_ = "{episode} - {date}".format( episode=self.episode and self.episode.title, date=self.local_start.strftime("%Y/%m/%d %H:%M%z"), ) if self.initial: str_ += " ({})".format(_("rerun")) return str_ def save(self, *args, **kwargs): super().save(*args, **kwargs) if self.is_initial and self.episode != self._initial["episode"]: self.rerun_set.update(episode=self.episode, program=self.program) # def save(self, no_check=False, *args, **kwargs): # if self.start != self._initial['start'] or \ # self.end != self._initial['end']: # self.check_conflicts() def save_rerun(self): self.episode = self.initial.episode super().save_rerun() def save_initial(self): self.program = self.episode.program @property def duration(self): return self.end - self.start @property def date(self): """Return diffusion start as a date.""" return utils.cast_date(self.start) @cached_property def local_start(self): """Return a version of self.date that is localized to self.timezone; This is needed since datetime are stored as UTC date and we want to get it as local time.""" return tz.localtime(self.start, tz.get_current_timezone()) @property def local_end(self): """Return a version of self.date that is localized to self.timezone; This is needed since datetime are stored as UTC date and we want to get it as local time.""" return tz.localtime(self.end, tz.get_current_timezone()) @property def is_now(self): """True if diffusion is currently running.""" now = tz.now() return ( self.type == self.TYPE_ON_AIR and self.start <= now and self.end >= now ) @property def is_live(self): """True if Diffusion is live (False if there are sounds files).""" return ( self.type == self.TYPE_ON_AIR and not self.episode.sound_set.archive().count() ) def get_playlist(self, **types): """Returns sounds as a playlist (list of *local* archive file path). The given arguments are passed to ``get_sounds``. """ from .sound import Sound return list( self.get_sounds(**types) .filter(path__isnull=False, type=Sound.TYPE_ARCHIVE) .values_list("path", flat=True) ) def get_sounds(self, **types): """Return a queryset of sounds related to this diffusion, ordered by type then path. **types: filter on the given sound types name, as `archive=True` """ from .sound import Sound sounds = (self.initial or self).sound_set.order_by("type", "path") _in = [ getattr(Sound.Type, name) for name, value in types.items() if value ] return sounds.filter(type__in=_in) def is_date_in_range(self, date=None): """Return true if the given date is in the diffusion's start-end range.""" date = date or tz.now() return self.start < date < self.end def get_conflicts(self): """Return conflicting diffusions queryset.""" # conflicts=Diffusion.objects.filter( # Q(start__lt=OuterRef('start'), end__gt=OuterRef('end')) | # Q(start__gt=OuterRef('start'), start__lt=OuterRef('end')) # ) # diffs= Diffusion.objects.annotate(conflict_with=Exists(conflicts)) # .filter(conflict_with=True) return ( Diffusion.objects.filter( Q(start__lt=self.start, end__gt=self.start) | Q(start__gt=self.start, start__lt=self.end) ) .exclude(pk=self.pk) .distinct() ) def check_conflicts(self): conflicts = self.get_conflicts() self.conflicts.set(conflicts) _initial = None def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._initial = { "start": self.start, "end": self.end, "episode": getattr(self, "episode", None), }