import datetime from enum import IntEnum from django.db import models from django.db.models import F, Q from django.db.models.functions import Concat, Substr from django.utils import timezone as tz from django.utils.translation import ugettext_lazy as _ from django.utils.functional import cached_property from aircox import settings, utils from .program import Program, BaseRerun, BaseRerunQuerySet from .page import Page, PageQuerySet __all__ = ['Episode', 'EpisodeQuerySet', 'Diffusion', 'DiffusionQuerySet'] class EpisodeQuerySet(PageQuerySet): def station(self, station): return self.filter(program__station=station) # FIXME: useful??? might use program.episode_set def program(self, program): return self.filter(program=program) class Episode(Page): program = models.ForeignKey( Program, models.CASCADE, verbose_name=_('program'), ) objects = EpisodeQuerySet.as_manager() class Meta: verbose_name = _('Episode') verbose_name_plural = _('Episodes') def save(self, *args, **kwargs): if self.cover is None: self.cover = self.program.cover super().save(*args, **kwargs) @classmethod def from_date(cls, program, date): title = settings.AIRCOX_EPISODE_TITLE.format( program=program, date=date.strftime(settings.AIRCOX_EPISODE_TITLE_DATE_FORMAT), ) return cls(program=program, title=title) class DiffusionQuerySet(BaseRerunQuerySet): def station(self, station): return self.filter(episode__program__station=station) def program(self, program): return self.filter(program=program) def on_air(self): return self.filter(type=Diffusion.Type.on_air) def at(self, date=None): """ Return diffusions occuring at the given date, ordered by +start If date is a datetime instance, get diffusions that occurs at the given moment. If date is not a datetime object, it uses it as a date, and get diffusions that occurs this day. When date is None, uses tz.now(). """ # note: we work with localtime date = utils.date_or_default(date) qs = self filters = None if isinstance(date, datetime.datetime): # use datetime: we want diffusion that occurs around this # range filters = {'start__lte': date, 'end__gte': date} qs = qs.filter(**filters) else: # use date: we want diffusions that occurs this day qs = qs.filter(Q(start__date=date) | Q(end__date=date)) return qs.order_by('start').distinct() 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(start__gte=date) else: qs = self.filter(start__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(BaseRerun): """ 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() class Type(IntEnum): on_air = 0x00 unconfirmed = 0x01 canceled = 0x02 episode = models.ForeignKey( Episode, models.CASCADE, verbose_name=_('episode'), ) type = models.SmallIntegerField( verbose_name=_('type'), default=Type.on_air, choices=[(int(y), _(x.replace('_', ' '))) for x, y in Type.__members__.items()], ) start = models.DateTimeField(_('start')) end = models.DateTimeField(_('end')) # port = models.ForeignKey( # 'self', # verbose_name = _('port'), # blank = True, null = True, # on_delete=models.SET_NULL, # help_text = _('use this input port'), # ) class Meta: verbose_name = _('Diffusion') verbose_name_plural = _('Diffusions') permissions = ( ('programming', _('edit the diffusion\'s planification')), ) def __str__(self): str_ = '{episode} - {date}'.format( self=self, 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, 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 self.program = self.episode.program def save_original(self): self.program = self.episode.program if self.episode != self._initial['episode']: self.rerun_set.update(episode=self.episode, program=self.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 original(self): """ Return the original diffusion (self or initial) """ return self.initial.original if self.initial else self # TODO: 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.get_sounds(archive=True).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), }