forked from rc/aircox
work on website + page becomes concrete
This commit is contained in:
@ -1,24 +1,10 @@
|
||||
# Aircox Programs
|
||||
|
||||
This application defines all base models and basic control of them. We have:
|
||||
* **Nameable**: generic class used in any class needing to be named. Includes some utility functions;
|
||||
* **Station**: a station
|
||||
* **Program**: the program itself;
|
||||
* **Diffusion**: occurrence of a program planified in the timetable. For rerun, informations are bound to the initial diffusion;
|
||||
* **Schedule**: describes diffusions frequencies for each program;
|
||||
* **Track**: track informations in a playlist of a diffusion;
|
||||
* **Sound**: information about a sound that can be used for podcast or rerun;
|
||||
* **Log**: logs
|
||||
|
||||
# Aircox
|
||||
Aircox application aims to provide basis of a radio management system.
|
||||
|
||||
## Architecture
|
||||
A Station is basically an object that represent a radio station. On each station, we use the Program object, that is declined in two different types:
|
||||
* **Scheduled**: the diffusion is based on a timetable and planified through one Schedule or more; Diffusion object represent the occurrence of these programs;
|
||||
* **Streamed**: the diffusion is based on random playlist, used to fill gaps between the programs;
|
||||
A Station contains programs that can be scheduled or streamed. A *Scheduled Program* is a regular show that has planified diffusions of its occurences (episodes). A *Streamed Program* is a program used to play randoms musics between the shows.
|
||||
|
||||
Each program has a directory in **AIRCOX_PROGRAMS_DIR**; For each, subdir:
|
||||
* **archives**: complete episode record, can be used for diffusions or as a podcast
|
||||
* **excerpts**: excerpt of an episode, or other elements, can be used as a podcast
|
||||
Each program has a directory on the server where user puts its podcasts (in **AIRCOX_PROGRAM_DIR**). It contains the directories **archives** (complete show's podcasts) and **excerpts** (partial or whatever podcasts).
|
||||
|
||||
|
||||
## manage.py's commands
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -25,6 +25,8 @@ class PageAdmin(admin.ModelAdmin):
|
||||
list_editable = ('status', 'category')
|
||||
prepopulated_fields = {"slug": ("title",)}
|
||||
|
||||
change_form_template = 'admin/aircox/page_change_form.html'
|
||||
|
||||
fieldsets = [
|
||||
('', {
|
||||
'fields': ['title', 'slug', 'category', 'cover', 'content'],
|
||||
|
@ -78,18 +78,14 @@ class Streamer:
|
||||
@property
|
||||
def inputs(self):
|
||||
""" Return input ports of the station """
|
||||
return self.station.port_set.filter(
|
||||
direction=Port.Direction.input,
|
||||
active=True
|
||||
)
|
||||
return self.station.port_set.filter(direction=Port.DIRECTION_INPUT,
|
||||
active=True)
|
||||
|
||||
@property
|
||||
def outputs(self):
|
||||
""" Return output ports of the station """
|
||||
return self.station.port_set.filter(
|
||||
direction=Port.Direction.output,
|
||||
active=True,
|
||||
)
|
||||
return self.station.port_set.filter(direction=Port.DIRECTION_OUTPUT,
|
||||
active=True)
|
||||
|
||||
# Sources and config ###############################################
|
||||
def send(self, *args, **kwargs):
|
||||
|
@ -57,14 +57,14 @@ class Actions:
|
||||
diffusion.save()
|
||||
|
||||
def clean(self):
|
||||
qs = Diffusion.objects.filter(type=Diffusion.Type.unconfirmed,
|
||||
qs = Diffusion.objects.filter(type=Diffusion.TYPE_UNCONFIRMED,
|
||||
start__lt=self.date)
|
||||
logger.info('[clean] %d diffusions will be removed', qs.count())
|
||||
qs.delete()
|
||||
|
||||
def check(self):
|
||||
# TODO: redo
|
||||
qs = Diffusion.objects.filter(type=Diffusion.Type.unconfirmed,
|
||||
qs = Diffusion.objects.filter(type=Diffusion.TYPE_UNCONFIRMED,
|
||||
start__gt=self.date)
|
||||
items = []
|
||||
for diffusion in qs:
|
||||
|
@ -184,9 +184,9 @@ class MonitorHandler(PatternMatchingEventHandler):
|
||||
"""
|
||||
self.subdir = subdir
|
||||
if self.subdir == settings.AIRCOX_SOUND_ARCHIVES_SUBDIR:
|
||||
self.sound_kwargs = {'type': Sound.Type.archive}
|
||||
self.sound_kwargs = {'type': Sound.TYPE_ARCHIVE}
|
||||
else:
|
||||
self.sound_kwargs = {'type': Sound.Type.excerpt}
|
||||
self.sound_kwargs = {'type': Sound.TYPE_EXCERPT}
|
||||
|
||||
patterns = ['*/{}/*{}'.format(self.subdir, ext)
|
||||
for ext in settings.AIRCOX_SOUND_FILE_EXT]
|
||||
@ -213,7 +213,7 @@ class MonitorHandler(PatternMatchingEventHandler):
|
||||
sound = Sound.objects.filter(path=event.src_path)
|
||||
if sound:
|
||||
sound = sound[0]
|
||||
sound.type = sound.Type.removed
|
||||
sound.type = sound.TYPE_REMOVED
|
||||
sound.save()
|
||||
|
||||
def on_moved(self, event):
|
||||
@ -259,11 +259,11 @@ class Command(BaseCommand):
|
||||
logger.info('#%d %s', program.id, program.title)
|
||||
self.scan_for_program(
|
||||
program, settings.AIRCOX_SOUND_ARCHIVES_SUBDIR,
|
||||
type=Sound.Type.archive,
|
||||
type=Sound.TYPE_ARCHIVE,
|
||||
)
|
||||
self.scan_for_program(
|
||||
program, settings.AIRCOX_SOUND_EXCERPTS_SUBDIR,
|
||||
type=Sound.Type.excerpt,
|
||||
type=Sound.TYPE_EXCERPT,
|
||||
)
|
||||
dirs.append(os.path.join(program.path))
|
||||
|
||||
@ -317,7 +317,7 @@ class Command(BaseCommand):
|
||||
|
||||
# get available sound files
|
||||
sounds = Sound.objects.filter(is_good_quality=False) \
|
||||
.exclude(type=Sound.Type.removed)
|
||||
.exclude(type=Sound.TYPE_REMOVED)
|
||||
if check:
|
||||
self.check_sounds(sounds)
|
||||
|
||||
|
@ -152,7 +152,7 @@ class Monitor:
|
||||
.now(air_time).first()
|
||||
|
||||
# log sound on air
|
||||
return self.log(type=Log.Type.on_air, date=source.air_time,
|
||||
return self.log(type=Log.TYPE_ON_AIR, date=source.air_time,
|
||||
source=source.id, sound=sound, diffusion=diff,
|
||||
comment=air_uri)
|
||||
|
||||
@ -177,7 +177,7 @@ class Monitor:
|
||||
if pos > now:
|
||||
break
|
||||
# log track on air
|
||||
self.log(type=Log.Type.on_air, date=pos, source=log.source,
|
||||
self.log(type=Log.TYPE_ON_AIR, date=pos, source=log.source,
|
||||
track=track, comment=track)
|
||||
|
||||
def handle_diffusions(self):
|
||||
@ -208,7 +208,7 @@ class Monitor:
|
||||
#
|
||||
now = tz.now()
|
||||
diff = Diffusion.objects.station(self.station).on_air().now(now) \
|
||||
.filter(episode__sound__type=Sound.Type.archive) \
|
||||
.filter(episode__sound__type=Sound.TYPE_ARCHIVE) \
|
||||
.first()
|
||||
# Can't use delay: diffusion may start later than its assigned start.
|
||||
log = None if not diff else self.logs.start().filter(diffusion=diff)
|
||||
@ -228,13 +228,13 @@ class Monitor:
|
||||
def start_diff(self, source, diff):
|
||||
playlist = Sound.objects.episode(id=diff.episode_id).paths()
|
||||
source.append(*playlist)
|
||||
self.log(type=Log.Type.start, source=source.id, diffusion=diff,
|
||||
self.log(type=Log.TYPE_START, source=source.id, diffusion=diff,
|
||||
comment=str(diff))
|
||||
|
||||
def cancel_diff(self, source, diff):
|
||||
diff.type = Diffusion.Type.cancel
|
||||
diff.type = Diffusion.TYPE_CANCEL
|
||||
diff.save()
|
||||
self.log(type=Log.Type.cancel, source=source.id, diffusion=diff,
|
||||
self.log(type=Log.TYPE_CANCEL, source=source.id, diffusion=diff,
|
||||
comment=str(diff))
|
||||
|
||||
def sync(self):
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,11 +1,17 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .page import Page
|
||||
from .page import Page, PageQuerySet
|
||||
from .program import Program, InProgramQuerySet
|
||||
|
||||
|
||||
class ArticleQuerySet(InProgramQuerySet, PageQuerySet):
|
||||
pass
|
||||
|
||||
|
||||
class Article(Page):
|
||||
detail_url_name = 'article-detail'
|
||||
|
||||
program = models.ForeignKey(
|
||||
Program, models.SET_NULL,
|
||||
verbose_name=_('program'), blank=True, null=True,
|
||||
@ -17,7 +23,7 @@ class Article(Page):
|
||||
'instead of a blog article'),
|
||||
)
|
||||
|
||||
objects = InProgramQuerySet.as_manager()
|
||||
objects = ArticleQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Article')
|
||||
|
@ -1,9 +1,7 @@
|
||||
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.db.models import Q
|
||||
from django.utils import timezone as tz
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.functional import cached_property
|
||||
@ -64,7 +62,7 @@ class DiffusionQuerySet(BaseRerunQuerySet):
|
||||
|
||||
def on_air(self):
|
||||
""" On air diffusions """
|
||||
return self.filter(type=Diffusion.Type.on_air)
|
||||
return self.filter(type=Diffusion.TYPE_ON_AIR)
|
||||
|
||||
def now(self, now=None, order=True):
|
||||
""" Diffusions occuring now """
|
||||
@ -132,20 +130,20 @@ class Diffusion(BaseRerun):
|
||||
"""
|
||||
objects = DiffusionQuerySet.as_manager()
|
||||
|
||||
class Type(IntEnum):
|
||||
on_air = 0x00
|
||||
unconfirmed = 0x01
|
||||
cancel = 0x02
|
||||
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'),
|
||||
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()],
|
||||
verbose_name=_('type'), default=TYPE_ON_AIR, choices=TYPE_CHOICES,
|
||||
)
|
||||
start = models.DateTimeField(_('start'))
|
||||
end = models.DateTimeField(_('end'))
|
||||
@ -222,7 +220,7 @@ class Diffusion(BaseRerun):
|
||||
# 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 \
|
||||
return self.type == self.TYPE_ON_AIR and \
|
||||
not self.episode.sound_set.archive().count()
|
||||
|
||||
def get_playlist(self, **types):
|
||||
@ -232,7 +230,7 @@ class Diffusion(BaseRerun):
|
||||
"""
|
||||
from .sound import Sound
|
||||
return list(self.get_sounds(**types)
|
||||
.filter(path__isnull=False, type=Sound.Type.archive)
|
||||
.filter(path__isnull=False, type=Sound.TYPE_ARCHIVE)
|
||||
.values_list('path', flat=True))
|
||||
|
||||
def get_sounds(self, **types):
|
||||
|
@ -1,6 +1,4 @@
|
||||
from collections import deque
|
||||
import datetime
|
||||
from enum import IntEnum
|
||||
import logging
|
||||
import os
|
||||
|
||||
@ -9,7 +7,7 @@ from django.utils import timezone as tz
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
from aircox import settings, utils
|
||||
from aircox import settings
|
||||
from .episode import Diffusion
|
||||
from .sound import Sound, Track
|
||||
from .station import Station
|
||||
@ -35,10 +33,10 @@ class LogQuerySet(models.QuerySet):
|
||||
self.filter(date__date__gte=date)
|
||||
|
||||
def on_air(self):
|
||||
return self.filter(type=Log.Type.on_air)
|
||||
return self.filter(type=Log.TYPE_ON_AIR)
|
||||
|
||||
def start(self):
|
||||
return self.filter(type=Log.Type.start)
|
||||
return self.filter(type=Log.TYPE_START)
|
||||
|
||||
def with_diff(self, with_it=True):
|
||||
return self.filter(diffusion__isnull=not with_it)
|
||||
@ -163,43 +161,33 @@ class Log(models.Model):
|
||||
This only remember what has been played on the outputs, not on each
|
||||
source; Source designate here which source is responsible of that.
|
||||
"""
|
||||
class Type(IntEnum):
|
||||
stop = 0x00
|
||||
"""
|
||||
Source has been stopped, e.g. manually
|
||||
"""
|
||||
# Rule: \/ diffusion != null \/ sound != null
|
||||
start = 0x01
|
||||
""" Diffusion or sound has been request to be played. """
|
||||
cancel = 0x02
|
||||
""" Diffusion has been canceled. """
|
||||
# Rule: \/ sound != null /\ track == null
|
||||
# \/ sound == null /\ track != null
|
||||
# \/ sound == null /\ track == null /\ comment = sound_path
|
||||
on_air = 0x03
|
||||
"""
|
||||
The sound or diffusion has been detected occurring on air. Can
|
||||
also designate live diffusion, although Liquidsoap did not play
|
||||
them since they don't have an attached sound archive.
|
||||
"""
|
||||
other = 0x04
|
||||
""" Other log """
|
||||
|
||||
TYPE_STOP = 0x00
|
||||
""" Source has been stopped, e.g. manually """
|
||||
# Rule: \/ diffusion != null \/ sound != null
|
||||
TYPE_START = 0x01
|
||||
""" Diffusion or sound has been request to be played. """
|
||||
TYPE_CANCEL = 0x02
|
||||
""" Diffusion has been canceled. """
|
||||
# Rule: \/ sound != null /\ track == null
|
||||
# \/ sound == null /\ track != null
|
||||
# \/ sound == null /\ track == null /\ comment = sound_path
|
||||
TYPE_ON_AIR = 0x03
|
||||
""" Sound or diffusion occured on air """
|
||||
TYPE_OTHER = 0x04
|
||||
""" Other log """
|
||||
TYPE_CHOICES = (
|
||||
(TYPE_STOP, _('stop')), (TYPE_START, _('start')),
|
||||
(TYPE_CANCEL, _('cancelled')), (TYPE_ON_AIR, _('on air')),
|
||||
(TYPE_OTHER, _('other'))
|
||||
)
|
||||
|
||||
station = models.ForeignKey(
|
||||
Station, models.CASCADE,
|
||||
verbose_name=_('station'),
|
||||
help_text=_('related station'),
|
||||
)
|
||||
type = models.SmallIntegerField(
|
||||
choices=[(int(y), _(x.replace('_', ' ')))
|
||||
for x, y in Type.__members__.items()],
|
||||
blank=True, null=True,
|
||||
verbose_name=_('type'),
|
||||
)
|
||||
date = models.DateTimeField(
|
||||
default=tz.now, db_index=True,
|
||||
verbose_name=_('date'),
|
||||
verbose_name=_('station'), help_text=_('related station'),
|
||||
)
|
||||
type = models.SmallIntegerField(_('type'), choices=TYPE_CHOICES)
|
||||
date = models.DateTimeField(_('date'), default=tz.now, db_index=True)
|
||||
source = models.CharField(
|
||||
# we use a CharField to avoid loosing logs information if the
|
||||
# source is removed
|
||||
|
@ -38,28 +38,30 @@ class Category(models.Model):
|
||||
|
||||
class PageQuerySet(InheritanceQuerySet):
|
||||
def draft(self):
|
||||
return self.filter(status=Page.STATUS.draft)
|
||||
return self.filter(status=Page.STATUS_DRAFT)
|
||||
|
||||
def published(self):
|
||||
return self.filter(status=Page.STATUS.published)
|
||||
return self.filter(status=Page.STATUS_PUBLISHED)
|
||||
|
||||
def trash(self):
|
||||
return self.filter(status=Page.STATUS.trash)
|
||||
return self.filter(status=Page.STATUS_TRASH)
|
||||
|
||||
|
||||
class Page(models.Model):
|
||||
""" Base class for publishable content """
|
||||
class STATUS(IntEnum):
|
||||
draft = 0x00
|
||||
published = 0x10
|
||||
trash = 0x20
|
||||
STATUS_DRAFT = 0x00
|
||||
STATUS_PUBLISHED = 0x10
|
||||
STATUS_TRASH = 0x20
|
||||
STATUS_CHOICES = (
|
||||
(STATUS_DRAFT, _('draft')),
|
||||
(STATUS_PUBLISHED, _('published')),
|
||||
(STATUS_TRASH, _('trash')),
|
||||
)
|
||||
|
||||
title = models.CharField(max_length=128)
|
||||
slug = models.SlugField(_('slug'), blank=True, unique=True)
|
||||
status = models.PositiveSmallIntegerField(
|
||||
_('status'),
|
||||
default=STATUS.draft,
|
||||
choices=[(int(y), _(x)) for x, y in STATUS.__members__.items()],
|
||||
_('status'), default=STATUS_DRAFT, choices=STATUS_CHOICES,
|
||||
)
|
||||
category = models.ForeignKey(
|
||||
Category, models.SET_NULL,
|
||||
@ -84,8 +86,6 @@ class Page(models.Model):
|
||||
|
||||
detail_url_name = None
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def __str__(self):
|
||||
return '{}: {}'.format(self._meta.verbose_name,
|
||||
@ -104,15 +104,15 @@ class Page(models.Model):
|
||||
|
||||
@property
|
||||
def is_draft(self):
|
||||
return self.status == self.STATUS.draft
|
||||
return self.status == self.STATUS_DRAFT
|
||||
|
||||
@property
|
||||
def is_published(self):
|
||||
return self.status == self.STATUS.published
|
||||
return self.status == self.STATUS_PUBLISHED
|
||||
|
||||
@property
|
||||
def is_trash(self):
|
||||
return self.status == self.STATUS.trash
|
||||
return self.status == self.STATUS_TRASH
|
||||
|
||||
@cached_property
|
||||
def headline(self):
|
||||
@ -132,6 +132,16 @@ class Page(models.Model):
|
||||
return cls(**cls.get_init_kwargs_from(page, **kwargs))
|
||||
|
||||
|
||||
class Comment(models.Model):
|
||||
page = models.ForeignKey(
|
||||
Page, models.CASCADE, verbose_name=_('related page'),
|
||||
)
|
||||
nickname = models.CharField(_('nickname'), max_length=32)
|
||||
email = models.EmailField(_('email'), max_length=32)
|
||||
date = models.DateTimeField(auto_now_add=True)
|
||||
content = models.TextField(_('content'), max_length=1024)
|
||||
|
||||
|
||||
class NavItem(models.Model):
|
||||
""" Navigation menu items """
|
||||
station = models.ForeignKey(
|
||||
|
@ -45,6 +45,11 @@ class Program(Page):
|
||||
Renaming a Program rename the corresponding directory to matches the new
|
||||
name if it does not exists.
|
||||
"""
|
||||
# explicit foreign key in order to avoid related name clashes
|
||||
page = models.OneToOneField(
|
||||
Page, models.CASCADE,
|
||||
parent_link=True, related_name='program_page'
|
||||
)
|
||||
station = models.ForeignKey(
|
||||
Station,
|
||||
verbose_name=_('station'),
|
||||
@ -478,7 +483,7 @@ class Schedule(BaseRerun):
|
||||
initial = diffusions[initial]
|
||||
|
||||
diffusions[date] = Diffusion(
|
||||
episode=episode, type=Diffusion.Type.on_air,
|
||||
episode=episode, type=Diffusion.TYPE_ON_AIR,
|
||||
initial=initial, start=date, end=date+duration
|
||||
)
|
||||
return episodes.values(), diffusions.values()
|
||||
|
@ -36,7 +36,7 @@ class SoundQuerySet(models.QuerySet):
|
||||
|
||||
def archive(self):
|
||||
""" Return sounds that are archives """
|
||||
return self.filter(type=Sound.Type.archive)
|
||||
return self.filter(type=Sound.TYPE_ARCHIVE)
|
||||
|
||||
def paths(self, archive=True, order_by=True):
|
||||
"""
|
||||
@ -55,11 +55,14 @@ class Sound(models.Model):
|
||||
A Sound is the representation of a sound file that can be either an excerpt
|
||||
or a complete archive of the related diffusion.
|
||||
"""
|
||||
class Type(IntEnum):
|
||||
other = 0x00,
|
||||
archive = 0x01,
|
||||
excerpt = 0x02,
|
||||
removed = 0x03,
|
||||
TYPE_OTHER = 0x00
|
||||
TYPE_ARCHIVE = 0x01
|
||||
TYPE_EXCERPT = 0x02
|
||||
TYPE_REMOVED = 0x03
|
||||
TYPE_CHOICES = (
|
||||
(TYPE_OTHER, _('other')), (TYPE_ARCHIVE, _('archive')),
|
||||
(TYPE_EXCERPT, _('excerpt')), (TYPE_REMOVED, _('removed'))
|
||||
)
|
||||
|
||||
name = models.CharField(_('name'), max_length=64)
|
||||
program = models.ForeignKey(
|
||||
@ -72,11 +75,7 @@ class Sound(models.Model):
|
||||
Episode, models.SET_NULL, blank=True, null=True,
|
||||
verbose_name=_('episode'),
|
||||
)
|
||||
type = models.SmallIntegerField(
|
||||
verbose_name=_('type'),
|
||||
choices=[(int(y), _(x)) for x, y in Type.__members__.items()],
|
||||
blank=True, null=True
|
||||
)
|
||||
type = models.SmallIntegerField(_('type'), choices=TYPE_CHOICES)
|
||||
# FIXME: url() does not use the same directory than here
|
||||
# should we use FileField for more reliability?
|
||||
path = models.FilePathField(
|
||||
@ -196,21 +195,21 @@ class Sound(models.Model):
|
||||
"""
|
||||
|
||||
if not self.file_exists():
|
||||
if self.type == self.Type.removed:
|
||||
if self.type == self.TYPE_REMOVED:
|
||||
return
|
||||
logger.info('sound %s: has been removed', self.path)
|
||||
self.type = self.Type.removed
|
||||
self.type = self.TYPE_REMOVED
|
||||
|
||||
return True
|
||||
|
||||
# not anymore removed
|
||||
changed = False
|
||||
|
||||
if self.type == self.Type.removed and self.program:
|
||||
if self.type == self.TYPE_REMOVED and self.program:
|
||||
changed = True
|
||||
self.type = self.Type.archive \
|
||||
self.type = self.TYPE_ARCHIVE \
|
||||
if self.path.startswith(self.program.archives_path) else \
|
||||
self.Type.excerpt
|
||||
self.TYPE_EXCERPT
|
||||
|
||||
# check mtime -> reset quality if changed (assume file changed)
|
||||
mtime = self.get_mtime()
|
||||
|
@ -1,4 +1,3 @@
|
||||
from enum import IntEnum
|
||||
import os
|
||||
|
||||
from django.db import models
|
||||
@ -91,36 +90,32 @@ class Port(models.Model):
|
||||
Some port types may be not available depending on the
|
||||
direction of the port.
|
||||
"""
|
||||
class Direction(IntEnum):
|
||||
input = 0x00
|
||||
output = 0x01
|
||||
DIRECTION_INPUT = 0x00
|
||||
DIRECTION_OUTPUT = 0x01
|
||||
DIRECTION_CHOICES = ((DIRECTION_INPUT, _('input')),
|
||||
(DIRECTION_OUTPUT, _('output')))
|
||||
|
||||
class Type(IntEnum):
|
||||
jack = 0x00
|
||||
alsa = 0x01
|
||||
pulseaudio = 0x02
|
||||
icecast = 0x03
|
||||
http = 0x04
|
||||
https = 0x05
|
||||
file = 0x06
|
||||
TYPE_JACK = 0x00
|
||||
TYPE_ALSA = 0x01
|
||||
TYPE_PULSEAUDIO = 0x02
|
||||
TYPE_ICECAST = 0x03
|
||||
TYPE_HTTP = 0x04
|
||||
TYPE_HTTPS = 0x05
|
||||
TYPE_FILE = 0x06
|
||||
TYPE_CHOICES = (
|
||||
(TYPE_JACK, 'jack'), (TYPE_ALSA, 'alsa'),
|
||||
(TYPE_PULSEAUDIO, 'pulseaudio'), (TYPE_ICECAST, 'icecast'),
|
||||
(TYPE_HTTP, 'http'), (TYPE_HTTPS, 'https'),
|
||||
(TYPE_FILE, _('file'))
|
||||
)
|
||||
|
||||
station = models.ForeignKey(
|
||||
Station,
|
||||
verbose_name=_('station'),
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
Station, models.CASCADE, verbose_name=_('station'))
|
||||
direction = models.SmallIntegerField(
|
||||
_('direction'),
|
||||
choices=[(int(y), _(x)) for x, y in Direction.__members__.items()],
|
||||
)
|
||||
type = models.SmallIntegerField(
|
||||
_('type'),
|
||||
# we don't translate the names since it is project names.
|
||||
choices=[(int(y), x) for x, y in Type.__members__.items()],
|
||||
)
|
||||
_('direction'), choices=DIRECTION_CHOICES)
|
||||
type = models.SmallIntegerField(_('type'), choices=TYPE_CHOICES)
|
||||
active = models.BooleanField(
|
||||
_('active'),
|
||||
default=True,
|
||||
_('active'), default=True,
|
||||
help_text=_('this port is active')
|
||||
)
|
||||
settings = models.TextField(
|
||||
@ -136,13 +131,13 @@ class Port(models.Model):
|
||||
Return True if the type is available for the given direction.
|
||||
"""
|
||||
|
||||
if self.direction == self.Direction.input:
|
||||
if self.direction == self.DIRECTION_INPUT:
|
||||
return self.type not in (
|
||||
self.Type.icecast, self.Type.file
|
||||
self.TYPE_ICECAST, self.TYPE_FILE
|
||||
)
|
||||
|
||||
return self.type not in (
|
||||
self.Type.http, self.Type.https
|
||||
self.TYPE_HTTP, self.TYPE_HTTPS
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
@ -7159,12 +7159,18 @@ label.panel-block {
|
||||
.is-borderless {
|
||||
border: none; }
|
||||
|
||||
.has-background-transparent {
|
||||
background-color: transparent; }
|
||||
|
||||
.navbar + .container {
|
||||
margin-top: 1em; }
|
||||
|
||||
.navbar.has-shadow, .navbar.is-fixed-bottom.has-shadow {
|
||||
box-shadow: 0em 0em 1em rgba(0, 0, 0, 0.1); }
|
||||
|
||||
a.navbar-item.is-active {
|
||||
border-bottom: 1px grey solid; }
|
||||
|
||||
/*
|
||||
.navbar-brand img {
|
||||
min-height: 6em;
|
||||
|
@ -419,7 +419,7 @@ eval("/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__,
|
||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
eval("/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"a\", function() { return render; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"b\", function() { return staticRenderFns; });\nvar render = function() {\n var _vm = this\n var _h = _vm.$createElement\n var _c = _vm._self._c || _h\n return _c(\n \"div\",\n [\n _c(\"div\", { staticClass: \"tabs is-centered\" }, [\n _c(\"ul\", [_vm._t(\"tabs\", null, { value: _vm.value })], 2)\n ]),\n _vm._v(\" \"),\n _vm._t(\"default\", null, { value: _vm.value })\n ],\n 2\n )\n}\nvar staticRenderFns = []\nrender._withStripped = true\n\n\n\n//# sourceURL=webpack:///./assets/vue/tabs.vue?./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options");
|
||||
eval("/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"a\", function() { return render; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"b\", function() { return staticRenderFns; });\nvar render = function() {\n var _vm = this\n var _h = _vm.$createElement\n var _c = _vm._self._c || _h\n return _c(\n \"div\",\n [\n _c(\"div\", { staticClass: \"tabs is-centered is-medium\" }, [\n _c(\"ul\", [_vm._t(\"tabs\", null, { value: _vm.value })], 2)\n ]),\n _vm._v(\" \"),\n _vm._t(\"default\", null, { value: _vm.value })\n ],\n 2\n )\n}\nvar staticRenderFns = []\nrender._withStripped = true\n\n\n\n//# sourceURL=webpack:///./assets/vue/tabs.vue?./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options");
|
||||
|
||||
/***/ })
|
||||
|
||||
|
32
aircox/templates/admin/aircox/page_change_form.html
Normal file
32
aircox/templates/admin/aircox/page_change_form.html
Normal file
@ -0,0 +1,32 @@
|
||||
{% extends "admin/change_form.html" %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block extrahead %}{{ block.super }}
|
||||
<link rel="stylesheet" type="text/css" href="{% static "aircox/vendor.css" %}"/>
|
||||
<link rel="stylesheet" type="text/css" href="{% static "aircox/main.css" %}"/>
|
||||
{% endblock %}
|
||||
|
||||
{% block submit_buttons_bottom %}
|
||||
{% if has_change_permission %}
|
||||
<div class="columns is-size-5">
|
||||
<div class="column has-text-left">
|
||||
{% if original and not original.is_trash %}
|
||||
<button type="submit" name="status" value="32" class="button is-danger is-size-6">{% trans "Move to trash" %}</button>
|
||||
{% endif %}
|
||||
{% if original and not original.is_draft %}
|
||||
<button type="submit" name="status" value="0" class="button is-warning is-size-6">{% trans "Mark as draft" %}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="column has-text-right">
|
||||
<button type="submit" class="button is-secondary is-size-6">{% trans "Save" %}</button>
|
||||
<button type="submit" name="_continue" class="button is-secondary is-size-6">{% trans "Save and continue" %}</button>
|
||||
{% if not original.is_published %}
|
||||
<button type="submit" name="status" value="16" class="button is-primary is-size-6">{% trans "Publish" %}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
29
aircox/templates/aircox/article_detail.html
Normal file
29
aircox/templates/aircox/article_detail.html
Normal file
@ -0,0 +1,29 @@
|
||||
{% extends "aircox/page.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block side_nav %}
|
||||
{{ block.super }}
|
||||
|
||||
{% if side_items %}
|
||||
<section>
|
||||
<h4 class="title is-4">{% trans "Latest news" %}</h4>
|
||||
|
||||
{% for object in side_items %}
|
||||
{% include "aircox/page_item.html" %}
|
||||
{% endfor %}
|
||||
|
||||
<br>
|
||||
<nav class="pagination is-centered">
|
||||
<ul class="pagination-list">
|
||||
<li>
|
||||
<a href="{% url "article-list" %}" class="pagination-link"
|
||||
aria-label="{% trans "Show all news" %}">
|
||||
{% trans "More news" %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
12
aircox/templates/aircox/article_list.html
Normal file
12
aircox/templates/aircox/article_list.html
Normal file
@ -0,0 +1,12 @@
|
||||
{% extends "aircox/page_list.html" %}
|
||||
{% load i18n aircox %}
|
||||
|
||||
{% block title %}
|
||||
{% if parent %}
|
||||
{% with parent.title as parent %}
|
||||
{% blocktrans %}Articles of {{ parent }}{% endblocktrans %}
|
||||
{% endwith %}
|
||||
{% else %}{{ block.super }}{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
@ -67,6 +67,7 @@ Context:
|
||||
|
||||
{% block main %}{% endblock main %}
|
||||
</main>
|
||||
|
||||
{% if show_side_nav %}
|
||||
<aside class="column is-one-third-desktop">
|
||||
{% block cover %}
|
||||
|
@ -1,20 +1,28 @@
|
||||
{% extends "aircox/page.html" %}
|
||||
{% load i18n aircox %}
|
||||
|
||||
{% block title %}{% trans "Timetable" %}{% endblock %}
|
||||
{% block title %}
|
||||
{% with station.name as station %}
|
||||
{% blocktrans %}This week's shows... {% endblocktrans %}
|
||||
{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
<div class="column">
|
||||
{% blocktrans %}From <b>{{ start }}</b> to <b>{{ end }}</b>{% endblocktrans %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{{ block.super }}
|
||||
|
||||
<section class="section">
|
||||
<h3 class="subtitle size-3">
|
||||
{% blocktrans %}From <b>{{ start }}</b> to <b>{{ end }}</b>{% endblocktrans %}
|
||||
</h3>
|
||||
|
||||
{% with True as hide_schedule %}
|
||||
<section>
|
||||
{% unique_id "timetable" as timetable_id %}
|
||||
<a-tabs default="{{ date }}">
|
||||
<template v-slot:tabs="scope" noscript="hidden">
|
||||
<li><a href="{% url "timetable" date=prev_date %}"><</a></li>
|
||||
<li><a href="{% url "timetable" date=prev_date %}">❬ {% trans "Before" %}</a></li>
|
||||
|
||||
{% for day in by_date.keys %}
|
||||
<a-tab value="{{ day }}">
|
||||
@ -25,11 +33,10 @@
|
||||
{% endfor %}
|
||||
|
||||
<li>
|
||||
<a href="{% url "timetable" date=next_date %}">></a>
|
||||
<a href="{% url "timetable" date=next_date %}">{% trans "After" %} ❭</a>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
{% with True as hide_schedule %}
|
||||
<template v-slot:default="{value}">
|
||||
{% for day, diffusions in by_date.items %}
|
||||
<noscript><h4 class="subtitle is-4">{{ day|date:"l d F Y" }}</h4></noscript>
|
||||
@ -51,8 +58,8 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
</template>
|
||||
{% endwith %}
|
||||
</a-tabs>
|
||||
</section>
|
||||
{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
|
@ -32,9 +32,9 @@
|
||||
{{ block.super }}
|
||||
|
||||
{% if podcasts or tracks %}
|
||||
<section class="columns is-desktop">
|
||||
<div class="columns is-desktop">
|
||||
{% if tracks %}
|
||||
<div class="column">
|
||||
<section class="column">
|
||||
<h4 class="title is-4">{% trans "Playlist" %}</h4>
|
||||
<ol>
|
||||
{% for track in tracks %}
|
||||
@ -46,17 +46,17 @@
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% if podcasts %}
|
||||
<div class="column">
|
||||
<section class="column">
|
||||
<h4 class="title is-4">{% trans "Podcasts" %}</h4>
|
||||
{% for object in podcasts %}
|
||||
{% include "aircox/podcast_item.html" %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
</section>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
@ -2,13 +2,11 @@
|
||||
{% load i18n aircox %}
|
||||
|
||||
{% block title %}
|
||||
{% if program %}
|
||||
{% with program.title as program %}
|
||||
{% blocktrans %}Episodes of {{ program }}{% endblocktrans %}
|
||||
{% if parent %}
|
||||
{% with parent.title as parent %}
|
||||
{% blocktrans %}Episodes of {{ parent }}{% endblocktrans %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% trans "Episodes" %}
|
||||
{% endif %}
|
||||
{% else %}{{ block.super }}{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
@ -11,7 +11,7 @@
|
||||
|
||||
<section class="section">
|
||||
{% if dates %}
|
||||
<nav class="tabs is-centered" aria-label="{% trans "Other days' logs" %}">
|
||||
<nav class="tabs is-medium is-centered" aria-label="{% trans "Other days' logs" %}">
|
||||
<ul>
|
||||
{% for day in dates %}
|
||||
<li {% if day == date %}class="is-active"{% endif %}>
|
||||
@ -30,7 +30,7 @@
|
||||
|
||||
{# <h4 class="subtitle size-4">{{ date }}</h4> #}
|
||||
{% with True as hide_schedule %}
|
||||
<table class="table is-striped is-hoverable is-fullwidth">
|
||||
<table class="table is-striped is-hoverable is-fullwidth has-background-transparent">
|
||||
{% for object in object_list %}
|
||||
<tr>
|
||||
<td>
|
||||
|
@ -16,7 +16,7 @@ Context:
|
||||
|
||||
{% block head_title %}
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
{% if title %} ‐ {% endif %}
|
||||
—
|
||||
{{ station.name }}
|
||||
{% endblock %}
|
||||
|
||||
|
@ -1,15 +1,14 @@
|
||||
{% extends "aircox/page.html" %}
|
||||
{% load i18n aircox %}
|
||||
|
||||
{% with view.model|verbose_name:True as model_name_plural %}
|
||||
|
||||
{% block title %}
|
||||
{{ model_name_plural }}
|
||||
{{ view.model|verbose_name:True|title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block side_nav %}
|
||||
{{ block.super }}
|
||||
|
||||
{% if filter_categories|length != 1 %}
|
||||
<section class="toolbar">
|
||||
<h4 class="subtitle is-5">{% trans "Filters" %}</h4>
|
||||
<form method="GET" action="">
|
||||
@ -51,6 +50,7 @@
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@ -97,5 +97,3 @@
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% endwith %}
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
||||
</noscript>
|
||||
|
||||
<a-player ref="player" src="{{ audio_streams.0 }}"
|
||||
live-info-url="{% url "api-live" %}" live-info-timeout="15"
|
||||
live-info-url="{% url "api-live" %}" :live-info-timeout="15"
|
||||
button-title="{% trans "Play/pause audio" %}">
|
||||
<template v-slot:sources>
|
||||
{% for stream in audio_streams %}
|
||||
|
@ -4,11 +4,11 @@
|
||||
{% block side_nav %}
|
||||
{{ block.super }}
|
||||
|
||||
{% if episodes %}
|
||||
{% if side_items %}
|
||||
<section>
|
||||
<h4 class="title is-4">{% trans "Last shows" %}</h4>
|
||||
|
||||
{% for object in episodes %}
|
||||
{% for object in side_items %}
|
||||
{% include "aircox/episode_item.html" %}
|
||||
{% endfor %}
|
||||
|
||||
@ -16,13 +16,14 @@
|
||||
<nav class="pagination is-centered">
|
||||
<ul class="pagination-list">
|
||||
<li>
|
||||
<a href="{% url "diffusion-list" program_slug=program.slug %}"
|
||||
<a href="{% url "diffusion-list" parent_slug=program.slug %}"
|
||||
class="pagination-link"
|
||||
aria-label="{% trans "Show all diffusions" %}">
|
||||
{% trans "All shows" %}
|
||||
aria-label="{% trans "Show all program's diffusions" %}">
|
||||
{% trans "More shows" %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
@ -6,3 +6,39 @@
|
||||
{% include "aircox/program_header.html" %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block main %}
|
||||
{{ block.super }}
|
||||
|
||||
<br>
|
||||
|
||||
{% with show_headline=False %}
|
||||
<div class="columns is-desktop">
|
||||
{% if articles %}
|
||||
<section class="column">
|
||||
<h4 class="title is-4">{% trans "Articles" %}</h4>
|
||||
|
||||
{% for object in articles %}
|
||||
{% include "aircox/page_item.html" %}
|
||||
{% endfor %}
|
||||
|
||||
<br>
|
||||
<nav class="pagination is-centered">
|
||||
<ul class="pagination-list">
|
||||
<li>
|
||||
<a href="{% url "article-list" parent_slug=program.slug %}"
|
||||
class="pagination-link"
|
||||
aria-label="{% trans "Show all program's articles" %}">
|
||||
{% trans "More articles" %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</section>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endwith %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
@ -30,26 +30,26 @@ urls = [
|
||||
views.ArticleListView.as_view(model=models.Article, is_static=False),
|
||||
name='article-list'),
|
||||
path(_('articles/<slug:slug>/'),
|
||||
views.PageDetailView.as_view(model=models.Article),
|
||||
views.ArticleDetailView.as_view(),
|
||||
name='article-detail'),
|
||||
|
||||
path(_('programs/'), views.PageListView.as_view(model=models.Program),
|
||||
name='program-list'),
|
||||
path(_('programs/<slug:slug>/'),
|
||||
views.ProgramDetailView.as_view(), name='program-detail'),
|
||||
path(_('programs/<slug:program_slug>/episodes/'),
|
||||
path(_('programs/<slug:parent_slug>/episodes/'),
|
||||
views.EpisodeListView.as_view(), name='diffusion-list'),
|
||||
path(_('programs/<slug:program_slug>/articles/'),
|
||||
path(_('programs/<slug:parent_slug>/articles/'),
|
||||
views.ArticleListView.as_view(), name='article-list'),
|
||||
|
||||
path(_('episodes/'),
|
||||
views.EpisodeListView.as_view(), name='diffusion-list'),
|
||||
path(_('episodes/week/'),
|
||||
views.TimetableView.as_view(), name='timetable'),
|
||||
path(_('episodes/week/<week:date>/'),
|
||||
views.TimetableView.as_view(), name='timetable'),
|
||||
path(_('episodes/<slug:slug>/'),
|
||||
views.EpisodeDetailView.as_view(), name='episode-detail'),
|
||||
path(_('week/'),
|
||||
views.TimetableView.as_view(), name='timetable'),
|
||||
path(_('week/<week:date>/'),
|
||||
views.TimetableView.as_view(), name='timetable'),
|
||||
|
||||
path(_('logs/'), views.LogListView.as_view(), name='logs'),
|
||||
path(_('logs/<date:date>/'), views.LogListView.as_view(), name='logs'),
|
||||
|
257
aircox/views.py
257
aircox/views.py
@ -1,257 +0,0 @@
|
||||
import os
|
||||
import json
|
||||
import datetime
|
||||
|
||||
from django.views.generic.base import View, TemplateResponseMixin
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import HttpResponse, Http404
|
||||
from django.shortcuts import render
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils import timezone as tz
|
||||
from django.views.decorators.cache import cache_page
|
||||
|
||||
import aircox.models as models
|
||||
|
||||
|
||||
# FIXME usefull?
|
||||
class Stations:
|
||||
stations = models.Station.objects.all()
|
||||
update_timeout = None
|
||||
fetch_timeout = None
|
||||
|
||||
def fetch(self):
|
||||
if self.fetch_timeout and self.fetch_timeout > tz.now():
|
||||
return
|
||||
|
||||
self.fetch_timeout = tz.now() + tz.timedelta(seconds=5)
|
||||
for station in self.stations:
|
||||
station.streamer.fetch()
|
||||
|
||||
|
||||
stations = Stations()
|
||||
|
||||
|
||||
@cache_page(10)
|
||||
def on_air(request):
|
||||
try:
|
||||
import aircox_cms.models as cms
|
||||
except:
|
||||
cms = None
|
||||
|
||||
station = request.GET.get('station')
|
||||
if station:
|
||||
# FIXME: by name???
|
||||
station = stations.stations.filter(name=station)
|
||||
if not station.count():
|
||||
return HttpResponse('{}')
|
||||
else:
|
||||
station = stations.stations
|
||||
|
||||
station = station.first()
|
||||
on_air = station.on_air(count=10).select_related('track', 'diffusion')
|
||||
if not on_air.count():
|
||||
return HttpResponse('')
|
||||
|
||||
last = on_air.first()
|
||||
if last.track:
|
||||
last = {'date': last.date, 'type': 'track',
|
||||
'artist': last.track.artist, 'title': last.track.title}
|
||||
else:
|
||||
try:
|
||||
diff = last.diffusion
|
||||
publication = None
|
||||
# FIXME CMS
|
||||
if cms:
|
||||
publication = \
|
||||
cms.DiffusionPage.objects.filter(
|
||||
diffusion=diff.initial or diff).first() or \
|
||||
cms.ProgramPage.objects.filter(
|
||||
program=last.program).first()
|
||||
except:
|
||||
pass
|
||||
last = {'date': diff.start, 'type': 'diffusion',
|
||||
'title': diff.program.name,
|
||||
'url': publication.specific.url if publication else None}
|
||||
last['date'] = str(last['date'])
|
||||
return HttpResponse(json.dumps(last))
|
||||
|
||||
|
||||
# TODO:
|
||||
# - login url
|
||||
class Monitor(View, TemplateResponseMixin, LoginRequiredMixin):
|
||||
template_name = 'aircox/controllers/monitor.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
stations.fetch()
|
||||
return {'stations': stations.stations}
|
||||
|
||||
def get(self, request=None, **kwargs):
|
||||
if not request.user.is_active:
|
||||
return Http404()
|
||||
|
||||
self.request = request
|
||||
context = self.get_context_data(**kwargs)
|
||||
return render(request, self.template_name, context)
|
||||
|
||||
def post(self, request=None, **kwargs):
|
||||
if not request.user.is_active:
|
||||
return Http404()
|
||||
|
||||
if not ('action' or 'station') in request.POST:
|
||||
return HttpResponse('')
|
||||
|
||||
POST = request.POST
|
||||
POST.get('controller')
|
||||
action = POST.get('action')
|
||||
|
||||
station = stations.stations.filter(name=POST.get('station')) \
|
||||
.first()
|
||||
if not station:
|
||||
return Http404()
|
||||
|
||||
source = None
|
||||
if 'source' in POST:
|
||||
source = [s for s in station.sources
|
||||
if s.name == POST['source']]
|
||||
source = source[0]
|
||||
if not source:
|
||||
return Http404
|
||||
|
||||
station.streamer.fetch()
|
||||
source = source or station.streamer.source
|
||||
if action == 'skip':
|
||||
self.actionSkip(request, station, source)
|
||||
if action == 'restart':
|
||||
self.actionRestart(request, station, source)
|
||||
return HttpResponse('')
|
||||
|
||||
def actionSkip(self, request, station, source):
|
||||
source.skip()
|
||||
|
||||
def actionRestart(self, request, station, source):
|
||||
source.restart()
|
||||
|
||||
|
||||
class StatisticsView(View, TemplateResponseMixin, LoginRequiredMixin):
|
||||
"""
|
||||
View for statistics.
|
||||
"""
|
||||
# we cannot manipulate queryset: we have to be able to read from archives
|
||||
template_name = 'aircox/controllers/stats.html'
|
||||
|
||||
class Item:
|
||||
date = None
|
||||
end = None
|
||||
name = None
|
||||
related = None
|
||||
tracks = None
|
||||
tags = None
|
||||
col = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
class Stats:
|
||||
station = None
|
||||
date = None
|
||||
items = None
|
||||
"""
|
||||
Log or Diffusion object that has been diffused by date. These
|
||||
objects have extra fields:
|
||||
- tags: [ (tag_name, tag_count), ...]
|
||||
- tracks_count: total count of tracks
|
||||
"""
|
||||
count = 0
|
||||
#rows = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.items = []
|
||||
# self.rows = []
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
# Note: one row contains a column for diffusions and one for streams
|
||||
# def append(self, log):
|
||||
# if log.col == 0:
|
||||
# self.rows.append((log, []))
|
||||
# return
|
||||
#
|
||||
# if self.rows:
|
||||
# row = self.rows[len(self.rows)-1]
|
||||
# last = row[0] or row[1][len(row[1])-1]
|
||||
# if last.date < log.date < last.end:
|
||||
# row[1].append(log)
|
||||
# return
|
||||
#
|
||||
# # all other cases: new row
|
||||
# self.rows.append((None, [log]))
|
||||
|
||||
def get_stats(self, station, date):
|
||||
"""
|
||||
Return statistics for the given station and date.
|
||||
"""
|
||||
stats = self.Stats(station=station, date=date,
|
||||
items=[], tags={})
|
||||
|
||||
qs = Log.objects.station(station).on_air() \
|
||||
.prefetch_related('diffusion', 'sound', 'track', 'track__tags')
|
||||
if not qs.exists():
|
||||
qs = models.Log.objects.load_archive(station, date)
|
||||
|
||||
sound_log = None
|
||||
for log in qs:
|
||||
rel, item = None, None
|
||||
if log.diffusion:
|
||||
rel, item = log.diffusion, self.Item(
|
||||
name=rel.program.name, type=_('Diffusion'), col=0,
|
||||
tracks=models.Track.objects.filter(diffusion=log.diffusion)
|
||||
.prefetch_related('tags'),
|
||||
)
|
||||
sound_log = None
|
||||
elif log.sound:
|
||||
rel, item = log.sound, self.Item(
|
||||
name=rel.program.name + ': ' + os.path.basename(rel.path),
|
||||
type=_('Stream'), col=1, tracks=[],
|
||||
)
|
||||
sound_log = item
|
||||
elif log.track:
|
||||
# append to last sound log
|
||||
if not sound_log:
|
||||
continue
|
||||
sound_log.tracks.append(log.track)
|
||||
sound_log.end = log.end
|
||||
continue
|
||||
|
||||
item.date = log.date
|
||||
item.end = log.end
|
||||
item.related = rel
|
||||
# stats.append(item)
|
||||
stats.items.append(item)
|
||||
return stats
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {}
|
||||
date = datetime.date.today()
|
||||
|
||||
try:
|
||||
GET = self.request.GET
|
||||
year = int(GET["year"]) if 'year' in GET else date.year
|
||||
month = int(GET["month"]) if 'month' in GET else date.month
|
||||
day = int(GET["day"]) if 'day' in GET else date.day
|
||||
date = datetime.date(year, month, day)
|
||||
except:
|
||||
pass
|
||||
|
||||
context["statistics"] = [
|
||||
self.get_stats(station, date)
|
||||
for station in models.Station.objects.all()
|
||||
]
|
||||
|
||||
return context
|
||||
|
||||
def get(self, request=None, **kwargs):
|
||||
if not request.user.is_active:
|
||||
return Http404()
|
||||
|
||||
self.request = request
|
||||
context = self.get_context_data(**kwargs)
|
||||
return render(request, self.template_name, context)
|
@ -1,6 +1,6 @@
|
||||
from . import api
|
||||
|
||||
from .article import ArticleListView
|
||||
from .article import ArticleDetailView, ArticleListView
|
||||
from .base import BaseView
|
||||
from .episode import EpisodeDetailView, EpisodeListView, TimetableView
|
||||
from .log import LogListView
|
||||
|
@ -1,16 +1,36 @@
|
||||
from ..models import Article
|
||||
from .program import ProgramPageListView
|
||||
from ..models import Article, Program
|
||||
from .page import ParentMixin, PageDetailView, PageListView
|
||||
|
||||
|
||||
__all__ = ['ArticleListView']
|
||||
__all__ = ['ArticleDetailView', 'ArticleListView']
|
||||
|
||||
|
||||
class ArticleListView(ProgramPageListView):
|
||||
class ArticleDetailView(PageDetailView):
|
||||
show_side_nav = True
|
||||
model = Article
|
||||
|
||||
def get_side_queryset(self):
|
||||
qs = Article.objects.select_related('cover') \
|
||||
.filter(is_static=False) \
|
||||
.order_by('-date')
|
||||
return qs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
if self.object.program is not None:
|
||||
kwargs.setdefault('parent', self.object.program)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class ArticleListView(ParentMixin, PageListView):
|
||||
model = Article
|
||||
template_name = 'aircox/article_list.html'
|
||||
show_headline = True
|
||||
is_static = False
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset(is_static=self.is_static)
|
||||
parent_model = Program
|
||||
fk_parent = 'program'
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(is_static=self.is_static)
|
||||
|
||||
|
||||
|
@ -6,17 +6,20 @@ from django.views.generic.base import TemplateResponseMixin, ContextMixin
|
||||
from ..utils import Redirect
|
||||
|
||||
|
||||
__all__ = ['BaseView', 'PageView']
|
||||
__all__ = ['BaseView']
|
||||
|
||||
|
||||
class BaseView(TemplateResponseMixin, ContextMixin):
|
||||
show_side_nav = False
|
||||
""" Show side navigation """
|
||||
title = None
|
||||
""" Page title """
|
||||
cover = None
|
||||
""" Page cover """
|
||||
|
||||
show_side_nav = False
|
||||
""" Show side navigation """
|
||||
list_count = 5
|
||||
""" Item count for small lists displayed on page. """
|
||||
|
||||
@property
|
||||
def station(self):
|
||||
return self.request.station
|
||||
@ -24,14 +27,24 @@ class BaseView(TemplateResponseMixin, ContextMixin):
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().station(self.station)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
def get_side_queryset(self):
|
||||
""" Return a queryset of items to render on the side nav. """
|
||||
return None
|
||||
|
||||
def get_context_data(self, side_items=None, **kwargs):
|
||||
kwargs.setdefault('station', self.station)
|
||||
kwargs.setdefault('cover', self.cover)
|
||||
kwargs.setdefault('show_side_nav', self.show_side_nav)
|
||||
|
||||
show_side_nav = kwargs.setdefault('show_side_nav', self.show_side_nav)
|
||||
if show_side_nav and side_items is None:
|
||||
side_items = self.get_side_queryset()
|
||||
side_items = None if side_items is None else \
|
||||
side_items[:self.list_count]
|
||||
|
||||
if not 'audio_streams' in kwargs:
|
||||
streams = self.station.audio_streams
|
||||
streams = streams and streams.split('\n')
|
||||
kwargs['audio_streams'] = streams
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
return super().get_context_data(side_items=side_items, **kwargs)
|
||||
|
||||
|
@ -1,16 +1,16 @@
|
||||
from collections import OrderedDict
|
||||
import datetime
|
||||
|
||||
from django.db.models import OuterRef, Subquery
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.views.generic import ListView
|
||||
|
||||
from ..models import Diffusion, Episode, Page, Program, Sound
|
||||
from ..converters import WeekConverter
|
||||
from ..models import Diffusion, Episode, Program, Sound
|
||||
from .base import BaseView
|
||||
from .program import ProgramPageDetailView, ProgramPageListView
|
||||
from .program import ProgramPageDetailView
|
||||
from .page import ParentMixin, PageListView
|
||||
|
||||
|
||||
__all__ = ['EpisodeDetailView', 'DiffusionListView', 'TimetableView']
|
||||
__all__ = ['EpisodeDetailView', 'EpisodeListView', 'TimetableView']
|
||||
|
||||
|
||||
class EpisodeDetailView(ProgramPageDetailView):
|
||||
@ -20,8 +20,9 @@ class EpisodeDetailView(ProgramPageDetailView):
|
||||
return Sound.objects.diffusion(diffusion).podcasts()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs.setdefault('program', self.object.program)
|
||||
kwargs.setdefault('parent', kwargs['program'])
|
||||
self.program = kwargs.setdefault('program', self.object.program)
|
||||
|
||||
kwargs.setdefault('parent', self.program)
|
||||
if not 'tracks' in kwargs:
|
||||
kwargs['tracks'] = self.object.track_set.order_by('position')
|
||||
if not 'podcasts' in kwargs:
|
||||
@ -29,12 +30,15 @@ class EpisodeDetailView(ProgramPageDetailView):
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class EpisodeListView(ProgramPageListView):
|
||||
class EpisodeListView(ParentMixin, PageListView):
|
||||
model = Episode
|
||||
template_name = 'aircox/diffusion_list.html'
|
||||
item_template_name = 'aircox/episode_item.html'
|
||||
show_headline = True
|
||||
|
||||
parent_model = Program
|
||||
fk_parent = 'program'
|
||||
|
||||
|
||||
class TimetableView(BaseView, ListView):
|
||||
""" View for timetables """
|
||||
|
@ -1,6 +1,7 @@
|
||||
|
||||
from django.core.exceptions import FieldDoesNotExist
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.views.generic import DetailView, ListView
|
||||
|
||||
from ..models import Category
|
||||
@ -8,41 +9,48 @@ from ..utils import Redirect
|
||||
from .base import BaseView
|
||||
|
||||
|
||||
__all__ = ['PageDetailView', 'PageListView']
|
||||
__all__ = ['ParentMixin', 'PageDetailView', 'PageListView']
|
||||
|
||||
|
||||
class PageDetailView(BaseView, DetailView):
|
||||
""" Base view class for pages. """
|
||||
context_object_name = 'page'
|
||||
class ParentMixin:
|
||||
"""
|
||||
Optional parent page for a list view. Parent is fetched and passed to the
|
||||
template context when `parent_model` is provided (queryset is filtered by
|
||||
parent page in such case).
|
||||
"""
|
||||
parent_model = None
|
||||
""" Parent model """
|
||||
parent_url_kwarg = 'parent_slug'
|
||||
""" Url lookup argument """
|
||||
parent_field = 'slug'
|
||||
""" Parent field for url lookup """
|
||||
fk_parent = 'page'
|
||||
""" Page foreign key to the parent """
|
||||
parent = None
|
||||
""" Parent page object """
|
||||
|
||||
def get_parent(self, request, *args, **kwargs):
|
||||
if self.parent_model is None or self.parent_url_kwarg not in kwargs:
|
||||
return
|
||||
|
||||
lookup = {self.parent_field: kwargs[self.parent_url_kwarg]}
|
||||
return get_object_or_404(
|
||||
self.parent_model.objects.select_related('cover'), **lookup)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.parent = self.get_parent(request, *args, **kwargs)
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().select_related('cover', 'category')
|
||||
|
||||
# This should not exists: it allows mapping not published pages
|
||||
# or it should be only used for trashed pages.
|
||||
def not_published_redirect(self, page):
|
||||
"""
|
||||
When a page is not published, redirect to the returned url instead
|
||||
of an HTTP 404 code. """
|
||||
return None
|
||||
|
||||
def get_object(self):
|
||||
obj = super().get_object()
|
||||
if not obj.is_published:
|
||||
redirect_url = self.not_published_redirect(obj)
|
||||
if redirect_url:
|
||||
raise Redirect(redirect_url)
|
||||
raise Http404('%s not found' % self.model._meta.verbose_name)
|
||||
return obj
|
||||
if self.parent is not None:
|
||||
lookup = {self.fk_parent: self.parent}
|
||||
return super().get_queryset().filter(**lookup)
|
||||
return super().get_queryset()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
#if kwargs.get('regions') is None:
|
||||
# contents = contents_for_item(
|
||||
# page, page_renderer._renderers.keys())
|
||||
# kwargs['regions'] = contents.render_regions(page_renderer)
|
||||
page = kwargs.setdefault('page', self.object)
|
||||
kwargs.setdefault('title', page.title)
|
||||
kwargs.setdefault('cover', page.cover)
|
||||
parent = kwargs.setdefault('parent', self.parent)
|
||||
if parent is not None:
|
||||
kwargs.setdefault('cover', parent.cover)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
@ -84,4 +92,35 @@ class PageListView(BaseView, ListView):
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class PageDetailView(BaseView, DetailView):
|
||||
""" Base view class for pages. """
|
||||
context_object_name = 'page'
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().select_related('cover', 'category')
|
||||
|
||||
# This should not exists: it allows mapping not published pages
|
||||
# or it should be only used for trashed pages.
|
||||
def not_published_redirect(self, page):
|
||||
"""
|
||||
When a page is not published, redirect to the returned url instead
|
||||
of an HTTP 404 code. """
|
||||
return None
|
||||
|
||||
def get_object(self):
|
||||
obj = super().get_object()
|
||||
if not obj.is_published:
|
||||
redirect_url = self.not_published_redirect(obj)
|
||||
if redirect_url:
|
||||
raise Redirect(redirect_url)
|
||||
raise Http404('%s not found' % self.model._meta.verbose_name)
|
||||
return obj
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
page = kwargs.setdefault('page', self.object)
|
||||
kwargs.setdefault('title', page.title)
|
||||
kwargs.setdefault('cover', page.cover)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
|
||||
|
@ -12,53 +12,25 @@ class ProgramPageDetailView(PageDetailView):
|
||||
"""
|
||||
Base view class for a page that is displayed as a program's child page.
|
||||
"""
|
||||
program = None
|
||||
show_side_nav = True
|
||||
list_count = 5
|
||||
|
||||
def get_episodes_queryset(self, program):
|
||||
return program.episode_set.published().order_by('-date')
|
||||
|
||||
def get_context_data(self, program, episodes=None, **kwargs):
|
||||
if episodes is None:
|
||||
episodes = self.get_episodes_queryset(program)
|
||||
return super().get_context_data(
|
||||
program=program, episodes=episodes[:self.list_count], **kwargs)
|
||||
|
||||
|
||||
class ProgramPageListView(PageListView):
|
||||
"""
|
||||
Base list view class rendering pages as a program's child page.
|
||||
Retrieved program from it slug provided by `kwargs['program_slug']`.
|
||||
|
||||
This view class can be used with or without providing a program.
|
||||
"""
|
||||
program = None
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
slug = kwargs.get('program_slug', None)
|
||||
if slug is not None:
|
||||
self.program = get_object_or_404(
|
||||
Program.objects.select_related('cover'), slug=slug)
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(program=self.program) \
|
||||
if self.program else super().get_queryset()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
program = kwargs.setdefault('program', self.program)
|
||||
if program is not None:
|
||||
kwargs.setdefault('cover', program.cover)
|
||||
kwargs.setdefault('parent', program)
|
||||
return super().get_context_data(**kwargs)
|
||||
def get_side_queryset(self):
|
||||
return self.program.episode_set.published().order_by('-date')
|
||||
|
||||
|
||||
class ProgramDetailView(ProgramPageDetailView):
|
||||
model = Program
|
||||
|
||||
def get_articles_queryset(self, program):
|
||||
return program.article_set.published().order_by('-date')
|
||||
def get_articles_queryset(self):
|
||||
return self.program.article_set.published().order_by('-date')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs.setdefault('program', self.object)
|
||||
self.program = kwargs.setdefault('program', self.object)
|
||||
if 'articles' not in kwargs:
|
||||
kwargs['articles'] = \
|
||||
self.get_articles_queryset()[:self.list_count]
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user