forked from rc/aircox
#93 Co-authored-by: bkfox <thomas bkfox net> Reviewed-on: rc/aircox#95
This commit is contained in:
@ -1,17 +1,11 @@
|
||||
from . import signals
|
||||
from .article import Article
|
||||
from .episode import Diffusion, DiffusionQuerySet, Episode
|
||||
from .diffusion import Diffusion, DiffusionQuerySet
|
||||
from .episode import Episode
|
||||
from .log import Log, LogArchiver, LogQuerySet
|
||||
from .page import Category, Comment, NavItem, Page, PageQuerySet, StaticPage
|
||||
from .program import (
|
||||
BaseRerun,
|
||||
BaseRerunQuerySet,
|
||||
Program,
|
||||
ProgramChildQuerySet,
|
||||
ProgramQuerySet,
|
||||
Schedule,
|
||||
Stream,
|
||||
)
|
||||
from .program import Program, ProgramChildQuerySet, ProgramQuerySet, Stream
|
||||
from .schedule import Schedule
|
||||
from .sound import Sound, SoundQuerySet, Track
|
||||
from .station import Port, Station, StationQuerySet
|
||||
from .user_settings import UserSettings
|
||||
@ -36,8 +30,6 @@ __all__ = (
|
||||
"Stream",
|
||||
"Schedule",
|
||||
"ProgramChildQuerySet",
|
||||
"BaseRerun",
|
||||
"BaseRerunQuerySet",
|
||||
"Sound",
|
||||
"SoundQuerySet",
|
||||
"Track",
|
||||
|
282
aircox/models/diffusion.py
Normal file
282
aircox/models/diffusion.py
Normal file
@ -0,0 +1,282 @@
|
||||
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.datetime.combine(date, datetime.time())
|
||||
end = 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),
|
||||
}
|
@ -1,24 +1,13 @@
|
||||
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 easy_thumbnails.files import get_thumbnailer
|
||||
|
||||
from aircox.conf import settings
|
||||
from aircox import utils
|
||||
|
||||
from .page import Page
|
||||
from .program import (
|
||||
BaseRerun,
|
||||
BaseRerunQuerySet,
|
||||
ProgramChildQuerySet,
|
||||
Schedule,
|
||||
)
|
||||
from .program import ProgramChildQuerySet
|
||||
|
||||
__all__ = ("Episode", "Diffusion", "DiffusionQuerySet")
|
||||
__all__ = ("Episode",)
|
||||
|
||||
|
||||
class Episode(Page):
|
||||
@ -90,269 +79,3 @@ class Episode(Page):
|
||||
return super().get_init_kwargs_from(
|
||||
page, title=title, program=page, **kwargs
|
||||
)
|
||||
|
||||
|
||||
class DiffusionQuerySet(BaseRerunQuerySet):
|
||||
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.datetime.combine(date, datetime.time())
|
||||
end = 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(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()
|
||||
|
||||
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
|
||||
self.program = self.episode.program
|
||||
|
||||
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),
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from aircox.conf import settings
|
||||
|
||||
__all__ = ("Settings", "settings")
|
||||
from .episode import Diffusion
|
||||
from .diffusion import Diffusion
|
||||
from .sound import Sound, Track
|
||||
from .station import Station
|
||||
|
||||
|
@ -1,21 +1,13 @@
|
||||
import calendar
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
from collections import OrderedDict
|
||||
from enum import IntEnum
|
||||
|
||||
import pytz
|
||||
from django.conf import settings as conf
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import F
|
||||
from django.db.models.functions import Concat, Substr
|
||||
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 aircox.conf import settings
|
||||
|
||||
from .page import Page, PageQuerySet
|
||||
@ -26,12 +18,9 @@ logger = logging.getLogger("aircox")
|
||||
|
||||
__all__ = (
|
||||
"Program",
|
||||
"ProgramChildQuerySet",
|
||||
"ProgramQuerySet",
|
||||
"Stream",
|
||||
"Schedule",
|
||||
"ProgramChildQuerySet",
|
||||
"BaseRerun",
|
||||
"BaseRerunQuerySet",
|
||||
)
|
||||
|
||||
|
||||
@ -167,352 +156,6 @@ class ProgramChildQuerySet(PageQuerySet):
|
||||
return self.parent(program, id)
|
||||
|
||||
|
||||
class BaseRerunQuerySet(models.QuerySet):
|
||||
"""Queryset for BaseRerun (sub)classes."""
|
||||
|
||||
def station(self, station=None, id=None):
|
||||
return (
|
||||
self.filter(program__station=station)
|
||||
if id is None
|
||||
else self.filter(program__station__id=id)
|
||||
)
|
||||
|
||||
def program(self, program=None, id=None):
|
||||
return (
|
||||
self.filter(program=program)
|
||||
if id is None
|
||||
else self.filter(program__id=id)
|
||||
)
|
||||
|
||||
def rerun(self):
|
||||
return self.filter(initial__isnull=False)
|
||||
|
||||
def initial(self):
|
||||
return self.filter(initial__isnull=True)
|
||||
|
||||
|
||||
class BaseRerun(models.Model):
|
||||
"""Abstract model offering rerun facilities.
|
||||
|
||||
Assume `start` is a datetime field or attribute implemented by
|
||||
subclass.
|
||||
"""
|
||||
|
||||
program = models.ForeignKey(
|
||||
Program,
|
||||
models.CASCADE,
|
||||
db_index=True,
|
||||
verbose_name=_("related program"),
|
||||
)
|
||||
initial = models.ForeignKey(
|
||||
"self",
|
||||
models.SET_NULL,
|
||||
related_name="rerun_set",
|
||||
verbose_name=_("rerun of"),
|
||||
limit_choices_to={"initial__isnull": True},
|
||||
blank=True,
|
||||
null=True,
|
||||
db_index=True,
|
||||
)
|
||||
|
||||
objects = BaseRerunQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.initial is not None:
|
||||
self.initial = self.initial.get_initial()
|
||||
if self.initial == self:
|
||||
self.initial = None
|
||||
|
||||
if self.is_rerun:
|
||||
self.save_rerun()
|
||||
else:
|
||||
self.save_initial()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def save_rerun(self):
|
||||
pass
|
||||
|
||||
def save_initial(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def is_initial(self):
|
||||
return self.initial is None
|
||||
|
||||
@property
|
||||
def is_rerun(self):
|
||||
return self.initial is not None
|
||||
|
||||
def get_initial(self):
|
||||
"""Return the initial schedule (self or initial)"""
|
||||
return self if self.initial is None else self.initial.get_initial()
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if self.initial is not None and self.initial.start >= self.start:
|
||||
raise ValidationError(
|
||||
{"initial": _("rerun must happen after original")}
|
||||
)
|
||||
|
||||
|
||||
# ? BIG FIXME: self.date is still used as datetime
|
||||
class Schedule(BaseRerun):
|
||||
"""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(IntEnum):
|
||||
ponctual = 0b000000
|
||||
first = 0b000001
|
||||
second = 0b000010
|
||||
third = 0b000100
|
||||
fourth = 0b001000
|
||||
last = 0b010000
|
||||
first_and_third = 0b000101
|
||||
second_and_fourth = 0b001010
|
||||
every = 0b011111
|
||||
one_on_two = 0b100000
|
||||
|
||||
date = models.DateField(
|
||||
_("date"),
|
||||
help_text=_("date of the first diffusion"),
|
||||
)
|
||||
time = models.TimeField(
|
||||
_("time"),
|
||||
help_text=_("start time"),
|
||||
)
|
||||
timezone = models.CharField(
|
||||
_("timezone"),
|
||||
default=tz.get_current_timezone,
|
||||
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=[
|
||||
(
|
||||
int(y),
|
||||
{
|
||||
"ponctual": _("ponctual"),
|
||||
"first": _("1st {day} of the month"),
|
||||
"second": _("2nd {day} of the month"),
|
||||
"third": _("3rd {day} of the month"),
|
||||
"fourth": _("4th {day} of the month"),
|
||||
"last": _("last {day} of the month"),
|
||||
"first_and_third": _("1st and 3rd {day} of the month"),
|
||||
"second_and_fourth": _("2nd and 4th {day} of the month"),
|
||||
"every": _("{day}"),
|
||||
"one_on_two": _("one {day} on two"),
|
||||
}[x],
|
||||
)
|
||||
for x, y in Frequency.__members__.items()
|
||||
],
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Schedule")
|
||||
verbose_name_plural = _("Schedules")
|
||||
|
||||
def __str__(self):
|
||||
return "{} - {}, {}".format(
|
||||
self.program.title,
|
||||
self.get_frequency_verbose(),
|
||||
self.time.strftime("%H:%M"),
|
||||
)
|
||||
|
||||
def save_rerun(self, *args, **kwargs):
|
||||
self.program = self.initial.program
|
||||
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_verbose(self):
|
||||
"""Return frequency formated for display."""
|
||||
from django.template.defaultfilters import date
|
||||
|
||||
return (
|
||||
self.get_frequency_display()
|
||||
.format(day=date(self.date, "l"))
|
||||
.capitalize()
|
||||
)
|
||||
|
||||
# initial cached data
|
||||
__initial = None
|
||||
|
||||
def changed(self, fields=["date", "duration", "frequency", "timezone"]):
|
||||
initial = self._Schedule__initial
|
||||
|
||||
if not initial:
|
||||
return
|
||||
|
||||
this = self.__dict__
|
||||
|
||||
for field in fields:
|
||||
if initial.get(field) != this.get(field):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
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 _exclude_existing_date(self, dates):
|
||||
from .episode import Diffusion
|
||||
|
||||
saved = set(
|
||||
Diffusion.objects.filter(start__in=dates).values_list(
|
||||
"start", flat=True
|
||||
)
|
||||
)
|
||||
return [date for date in dates if date not in saved]
|
||||
|
||||
def diffusions_of_month(self, date):
|
||||
"""Get episodes and diffusions for month of provided date, including
|
||||
reruns.
|
||||
|
||||
:returns: tuple([Episode], [Diffusion])
|
||||
"""
|
||||
from .episode import Diffusion, 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 = OrderedDict((date, None) for date in self.dates_of_month(date))
|
||||
dates.update(
|
||||
[
|
||||
(rerun.normalize(date.date() + delta), date)
|
||||
for date in 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()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# TODO/FIXME: use validators?
|
||||
if self.initial is not None and self.date > self.date:
|
||||
raise ValueError("initial must be later")
|
||||
|
||||
|
||||
class Stream(models.Model):
|
||||
"""When there are no program scheduled, it is possible to play sounds in
|
||||
order to avoid blanks. A Stream is a Program that plays this role, and
|
||||
|
106
aircox/models/rerun.py
Normal file
106
aircox/models/rerun.py
Normal file
@ -0,0 +1,106 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .program import Program
|
||||
|
||||
|
||||
__all__ = (
|
||||
"Rerun",
|
||||
"RerunQuerySet",
|
||||
)
|
||||
|
||||
|
||||
class RerunQuerySet(models.QuerySet):
|
||||
"""Queryset for Rerun (sub)classes."""
|
||||
|
||||
def station(self, station=None, id=None):
|
||||
return (
|
||||
self.filter(program__station=station)
|
||||
if id is None
|
||||
else self.filter(program__station__id=id)
|
||||
)
|
||||
|
||||
def program(self, program=None, id=None):
|
||||
return (
|
||||
self.filter(program=program)
|
||||
if id is None
|
||||
else self.filter(program__id=id)
|
||||
)
|
||||
|
||||
def rerun(self):
|
||||
return self.filter(initial__isnull=False)
|
||||
|
||||
def initial(self):
|
||||
return self.filter(initial__isnull=True)
|
||||
|
||||
|
||||
class Rerun(models.Model):
|
||||
"""Abstract model offering rerun facilities.
|
||||
|
||||
Assume `start` is a datetime field or attribute implemented by
|
||||
subclass.
|
||||
"""
|
||||
|
||||
program = models.ForeignKey(
|
||||
Program,
|
||||
models.CASCADE,
|
||||
db_index=True,
|
||||
verbose_name=_("related program"),
|
||||
)
|
||||
initial = models.ForeignKey(
|
||||
"self",
|
||||
models.SET_NULL,
|
||||
related_name="rerun_set",
|
||||
verbose_name=_("rerun of"),
|
||||
limit_choices_to={"initial__isnull": True},
|
||||
blank=True,
|
||||
null=True,
|
||||
db_index=True,
|
||||
)
|
||||
|
||||
objects = RerunQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@property
|
||||
def is_initial(self):
|
||||
return self.initial is None
|
||||
|
||||
@property
|
||||
def is_rerun(self):
|
||||
return self.initial is not None
|
||||
|
||||
def get_initial(self):
|
||||
"""Return the initial schedule (self or initial)"""
|
||||
return self if self.initial is None else self.initial.get_initial()
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if (
|
||||
hasattr(self, "start")
|
||||
and self.initial is not None
|
||||
and self.initial.start >= self.start
|
||||
):
|
||||
raise ValidationError(
|
||||
{"initial": _("rerun must happen after original")}
|
||||
)
|
||||
|
||||
def save_rerun(self):
|
||||
self.program = self.initial.program
|
||||
|
||||
def save_initial(self):
|
||||
pass
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.initial is not None:
|
||||
self.initial = self.initial.get_initial()
|
||||
if self.initial == self:
|
||||
self.initial = None
|
||||
|
||||
if self.is_rerun:
|
||||
self.save_rerun()
|
||||
else:
|
||||
self.save_initial()
|
||||
super().save(*args, **kwargs)
|
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()
|
@ -6,9 +6,11 @@ from django.utils import timezone as tz
|
||||
|
||||
from aircox import utils
|
||||
from aircox.conf import settings
|
||||
from .episode import Episode, Diffusion
|
||||
from .diffusion import Diffusion
|
||||
from .episode import Episode
|
||||
from .page import Page
|
||||
from .program import Program, Schedule
|
||||
from .program import Program
|
||||
from .schedule import Schedule
|
||||
|
||||
|
||||
# Add a default group to a user when it is created. It also assigns a list
|
||||
|
Reference in New Issue
Block a user