website
This commit is contained in:
parent
324cf2ef0f
commit
248b77fca4
|
@ -1,3 +1,2 @@
|
|||
|
||||
default_app_config = 'aircox.apps.AircoxConfig'
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from .article import ArticleAdmin
|
||||
from .episode import DiffusionAdmin, EpisodeAdmin
|
||||
from .log import LogAdmin
|
||||
# from .playlist import PlaylistAdmin
|
||||
from .program import ProgramAdmin, ScheduleAdmin, StreamAdmin
|
||||
from .sound import SoundAdmin
|
||||
from .sound import SoundAdmin, TrackAdmin
|
||||
from .station import StationAdmin
|
||||
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
20
aircox/admin/article.py
Normal file
20
aircox/admin/article.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
import copy
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from ..models import Article
|
||||
from .page import PageAdmin
|
||||
|
||||
|
||||
__all__ = ['ArticleAdmin']
|
||||
|
||||
|
||||
@admin.register(Article)
|
||||
class ArticleAdmin(PageAdmin):
|
||||
list_display = PageAdmin.list_display + ('program',)
|
||||
list_filter = ('program',)
|
||||
# TODO: readonly field
|
||||
|
||||
fieldsets = copy.deepcopy(PageAdmin.fieldsets)
|
||||
fieldsets[1][1]['fields'].insert(0, 'program')
|
||||
|
|
@ -6,8 +6,7 @@ from django.utils.translation import ugettext as _, ugettext_lazy
|
|||
from aircox.models import Episode, Diffusion, Sound, Track
|
||||
|
||||
from .page import PageAdmin
|
||||
from .playlist import TracksInline
|
||||
from .sound import SoundInline
|
||||
from .sound import SoundInline, TracksInline
|
||||
|
||||
|
||||
class DiffusionBaseAdmin:
|
||||
|
|
|
@ -4,18 +4,30 @@ from django.utils.translation import ugettext_lazy as _
|
|||
|
||||
from adminsortable2.admin import SortableInlineAdminMixin
|
||||
|
||||
from ..models import NavItem
|
||||
from ..models import Category, Article, NavItem
|
||||
|
||||
|
||||
__all__ = ['CategoryAdmin', 'PageAdmin', 'NavItemInline']
|
||||
|
||||
|
||||
@admin.register(Category)
|
||||
class CategoryAdmin(admin.ModelAdmin):
|
||||
list_display = ['pk', 'title', 'slug']
|
||||
list_editable = ['title', 'slug']
|
||||
fields = ['title', 'slug']
|
||||
prepopulated_fields = {"slug": ("title",)}
|
||||
|
||||
|
||||
# limit category choice
|
||||
class PageAdmin(admin.ModelAdmin):
|
||||
list_display = ('cover_thumb', 'title', 'status')
|
||||
list_display = ('cover_thumb', 'title', 'status', 'category')
|
||||
list_display_links = ('cover_thumb', 'title')
|
||||
list_editable = ('status',)
|
||||
list_editable = ('status', 'category')
|
||||
prepopulated_fields = {"slug": ("title",)}
|
||||
|
||||
fieldsets = [
|
||||
('', {
|
||||
'fields': ['title', 'slug', 'cover', 'content'],
|
||||
'fields': ['title', 'slug', 'category', 'cover', 'content'],
|
||||
}),
|
||||
(_('Publication Settings'), {
|
||||
'fields': ['featured', 'allow_comments', 'status'],
|
||||
|
@ -31,3 +43,5 @@ class PageAdmin(admin.ModelAdmin):
|
|||
class NavItemInline(SortableInlineAdminMixin, admin.TabularInline):
|
||||
model = NavItem
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
from django.contrib import admin
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
|
||||
from adminsortable2.admin import SortableInlineAdminMixin
|
||||
|
||||
from aircox.models import Track
|
||||
|
||||
|
||||
class TracksInline(SortableInlineAdminMixin, admin.TabularInline):
|
||||
template = 'admin/aircox/playlist_inline.html'
|
||||
model = Track
|
||||
extra = 0
|
||||
fields = ('position', 'artist', 'title', 'info', 'timestamp', 'tags')
|
||||
|
||||
list_display = ['artist', 'title', 'tags', 'related']
|
||||
list_filter = ['artist', 'title', 'tags']
|
||||
|
||||
|
||||
@admin.register(Track)
|
||||
class TrackAdmin(admin.ModelAdmin):
|
||||
def tag_list(self, obj):
|
||||
return u", ".join(o.name for o in obj.tags.all())
|
||||
|
||||
list_display = ['pk', 'artist', 'title', 'tag_list', 'episode', 'sound', 'timestamp']
|
||||
list_editable = ['artist', 'title']
|
||||
list_filter = ['sound', 'episode', 'artist', 'title', 'tags']
|
||||
fieldsets = [
|
||||
(_('Playlist'), {'fields': ['episode', 'sound', 'position', 'timestamp']}),
|
||||
(_('Info'), {'fields': ['artist', 'title', 'info', 'tags']}),
|
||||
]
|
||||
|
||||
# TODO on edit: readonly_fields = ['episode', 'sound']
|
||||
|
||||
#@admin.register(Playlist)
|
||||
#class PlaylistAdmin(admin.ModelAdmin):
|
||||
# fields = ['episode', 'sound']
|
||||
# inlines = [TracksInline]
|
||||
# # TODO: dynamic read only fields
|
||||
|
||||
|
|
@ -1,8 +1,20 @@
|
|||
from django.contrib import admin
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
|
||||
from aircox.models import Sound
|
||||
from .playlist import TracksInline
|
||||
from adminsortable2.admin import SortableInlineAdminMixin
|
||||
|
||||
from aircox.models import Sound, Track
|
||||
|
||||
|
||||
class TracksInline(SortableInlineAdminMixin, admin.TabularInline):
|
||||
template = 'admin/aircox/playlist_inline.html'
|
||||
model = Track
|
||||
extra = 0
|
||||
fields = ('position', 'artist', 'title', 'info', 'timestamp', 'tags')
|
||||
|
||||
list_display = ['artist', 'title', 'tags', 'related']
|
||||
list_filter = ['artist', 'title', 'tags']
|
||||
|
||||
|
||||
|
||||
class SoundInline(admin.TabularInline):
|
||||
|
@ -31,4 +43,19 @@ class SoundAdmin(admin.ModelAdmin):
|
|||
inlines = [TracksInline]
|
||||
|
||||
|
||||
@admin.register(Track)
|
||||
class TrackAdmin(admin.ModelAdmin):
|
||||
def tag_list(self, obj):
|
||||
return u", ".join(o.name for o in obj.tags.all())
|
||||
|
||||
list_display = ['pk', 'artist', 'title', 'tag_list', 'episode', 'sound', 'timestamp']
|
||||
list_editable = ['artist', 'title']
|
||||
list_filter = ['sound', 'episode', 'artist', 'title', 'tags']
|
||||
fieldsets = [
|
||||
(_('Playlist'), {'fields': ['episode', 'sound', 'position', 'timestamp']}),
|
||||
(_('Info'), {'fields': ['artist', 'title', 'info', 'tags']}),
|
||||
]
|
||||
|
||||
# TODO on edit: readonly_fields = ['episode', 'sound']
|
||||
|
||||
|
||||
|
|
|
@ -6,5 +6,4 @@ class AircoxConfig(AppConfig):
|
|||
verbose_name = 'Aircox'
|
||||
|
||||
def ready(self):
|
||||
import aircox.signals
|
||||
|
||||
pass
|
||||
|
|
|
@ -33,7 +33,7 @@ class Connector:
|
|||
return
|
||||
|
||||
family = socket.AF_UNIX if isinstance(self.address, str) else \
|
||||
socket.AF_INET
|
||||
socket.AF_INET
|
||||
try:
|
||||
self.socket = socket.socket(family, socket.SOCK_STREAM)
|
||||
self.socket.connect(self.address)
|
||||
|
@ -81,4 +81,3 @@ class Connector:
|
|||
return json.loads(value) if value else None
|
||||
except:
|
||||
return None
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
from collections import OrderedDict
|
||||
import atexit
|
||||
import logging
|
||||
import os
|
||||
|
@ -121,9 +120,6 @@ class Streamer:
|
|||
|
||||
def sync(self):
|
||||
""" Sync all sources. """
|
||||
if self.process is None:
|
||||
return
|
||||
|
||||
for source in self.sources:
|
||||
source.sync()
|
||||
|
||||
|
@ -141,7 +137,7 @@ class Streamer:
|
|||
return
|
||||
|
||||
self.source = next((source for source in self.sources
|
||||
if source.is_playing), None)
|
||||
if source.is_playing), None)
|
||||
|
||||
# Process ##########################################################
|
||||
def get_process_args(self):
|
||||
|
@ -214,8 +210,8 @@ class Source:
|
|||
def is_playing(self):
|
||||
return self.status == 'playing'
|
||||
|
||||
#@property
|
||||
#def is_on_air(self):
|
||||
# @property
|
||||
# def is_on_air(self):
|
||||
# return self.rid is not None and self.rid in self.controller.on_air
|
||||
|
||||
def __init__(self, controller, id=None):
|
||||
|
@ -224,7 +220,6 @@ class Source:
|
|||
|
||||
def sync(self):
|
||||
""" Synchronize what should be synchronized """
|
||||
pass
|
||||
|
||||
def fetch(self):
|
||||
data = self.controller.send(self.id, '.remaining')
|
||||
|
@ -322,5 +317,3 @@ class QueueSource(Source):
|
|||
super().fetch()
|
||||
queue = self.controller.send(self.id, '_queue.queue').split(' ')
|
||||
self.queue = queue
|
||||
|
||||
|
||||
|
|
|
@ -45,4 +45,3 @@ class DateConverter:
|
|||
def to_url(self, value):
|
||||
return '{:04d}/{:02d}/{:02d}'.format(value.year, value.month,
|
||||
value.day)
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import pytz
|
||||
from django import shortcuts
|
||||
from django.db.models import Q, Case, Value, When
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone as tz
|
||||
|
||||
from .models import Station
|
||||
|
@ -16,18 +15,18 @@ class AircoxMiddleware(object):
|
|||
This middleware must be set after the middleware
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
"""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def get_station(self, request):
|
||||
""" Return station for the provided request """
|
||||
expr = Q(default=True) | Q(hosts__contains=request.get_host())
|
||||
#case = Case(When(hosts__contains=request.get_host(), then=Value(0)),
|
||||
# case = Case(When(hosts__contains=request.get_host(), then=Value(0)),
|
||||
# When(default=True, then=Value(32)))
|
||||
return Station.objects.filter(expr).order_by('default').first()
|
||||
# .annotate(resolve_priority=case) \
|
||||
#.order_by('resolve_priority').first()
|
||||
|
||||
# .order_by('resolve_priority').first()
|
||||
|
||||
def init_timezone(self, request):
|
||||
# note: later we can use http://freegeoip.net/ on user side if
|
||||
|
@ -44,13 +43,10 @@ class AircoxMiddleware(object):
|
|||
timezone = tz.get_current_timezone()
|
||||
tz.activate(timezone)
|
||||
|
||||
|
||||
def __call__(self, request):
|
||||
self.init_timezone(request)
|
||||
request.station = self.get_station(request)
|
||||
try:
|
||||
return self.get_response(request)
|
||||
except Redirect as redirect:
|
||||
return
|
||||
|
||||
|
||||
except Redirect:
|
||||
return
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
from .page import Page, NavItem
|
||||
from .article import Article
|
||||
from .page import Category, Page, NavItem
|
||||
from .program import Program, Stream, Schedule
|
||||
from .episode import Episode, Diffusion
|
||||
from .log import Log
|
||||
from .sound import Sound, Track
|
||||
from .station import Station, Port
|
||||
|
||||
from . import signals
|
||||
|
||||
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
27
aircox/models/article.py
Normal file
27
aircox/models/article.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .page import Page
|
||||
from .program import Program, InProgramQuerySet
|
||||
|
||||
|
||||
class Article(Page):
|
||||
program = models.ForeignKey(
|
||||
Program, models.SET_NULL,
|
||||
verbose_name=_('program'), blank=True, null=True,
|
||||
help_text=_("publish as this program's article"),
|
||||
)
|
||||
is_static = models.BooleanField(
|
||||
_('is static'), default=False,
|
||||
help_text=_('Should this article be considered as a page '
|
||||
'instead of a blog article'),
|
||||
)
|
||||
|
||||
objects = InProgramQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Article')
|
||||
verbose_name_plural = _('Articles')
|
||||
|
||||
|
||||
|
|
@ -18,13 +18,17 @@ from .page import Page, PageQuerySet
|
|||
__all__ = ['Episode', 'Diffusion', 'DiffusionQuerySet']
|
||||
|
||||
|
||||
class EpisodeQuerySet(PageQuerySet, InProgramQuerySet):
|
||||
pass
|
||||
|
||||
|
||||
class Episode(Page):
|
||||
program = models.ForeignKey(
|
||||
Program, models.CASCADE,
|
||||
verbose_name=_('program'),
|
||||
)
|
||||
|
||||
objects = InProgramQuerySet.as_manager()
|
||||
objects = EpisodeQuerySet.as_manager()
|
||||
detail_url_name = 'episode-detail'
|
||||
|
||||
class Meta:
|
||||
|
@ -37,17 +41,14 @@ class Episode(Page):
|
|||
super().save(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def get_default_title(cls, program, date):
|
||||
def get_init_kwargs_from(cls, page, date, title=None, **kwargs):
|
||||
""" Get default Episode's title """
|
||||
return settings.AIRCOX_EPISODE_TITLE.format(
|
||||
program=program,
|
||||
title = settings.AIRCOX_EPISODE_TITLE.format(
|
||||
program=page,
|
||||
date=date.strftime(settings.AIRCOX_EPISODE_TITLE_DATE_FORMAT),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_date(cls, program, date):
|
||||
title = cls.get_default_title(program, date)
|
||||
return cls(program=program, title=title, cover=program.cover)
|
||||
) if title is None else title
|
||||
return super().get_init_kwargs_from(page, title=title, program=page,
|
||||
**kwargs)
|
||||
|
||||
|
||||
class DiffusionQuerySet(BaseRerunQuerySet):
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
from enum import IntEnum
|
||||
import re
|
||||
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone as tz
|
||||
from django.utils.text import slugify
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
from ckeditor.fields import RichTextField
|
||||
from filer.fields.image import FilerImageField
|
||||
|
@ -16,13 +16,36 @@ from model_utils.managers import InheritanceQuerySet
|
|||
from .station import Station
|
||||
|
||||
|
||||
__all__ = ['PageQuerySet', 'Page', 'NavItem']
|
||||
__all__ = ['Category', 'PageQuerySet', 'Page', 'NavItem']
|
||||
|
||||
|
||||
headline_re = re.compile(r'(<p>)?'
|
||||
r'(?P<headline>[^\n]{1,140}(\n|[^\.]*?\.))'
|
||||
r'(</p>)?')
|
||||
|
||||
|
||||
class Category(models.Model):
|
||||
title = models.CharField(_('title'), max_length=64)
|
||||
slug = models.SlugField(_('slug'), max_length=64, db_index=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Category')
|
||||
verbose_name_plural = _('Categories')
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
|
||||
class PageQuerySet(InheritanceQuerySet):
|
||||
def draft(self):
|
||||
return self.filter(status=Page.STATUS.draft)
|
||||
|
||||
def published(self):
|
||||
return self.filter(status=Page.STATUS.published)
|
||||
|
||||
def trash(self):
|
||||
return self.filter(status=Page.STATUS.trash)
|
||||
|
||||
|
||||
class Page(models.Model):
|
||||
""" Base class for publishable content """
|
||||
|
@ -38,13 +61,18 @@ class Page(models.Model):
|
|||
default=STATUS.draft,
|
||||
choices=[(int(y), _(x)) for x, y in STATUS.__members__.items()],
|
||||
)
|
||||
category = models.ForeignKey(
|
||||
Category, models.SET_NULL,
|
||||
verbose_name=_('category'), blank=True, null=True, db_index=True
|
||||
)
|
||||
cover = FilerImageField(
|
||||
on_delete=models.SET_NULL, null=True, blank=True,
|
||||
verbose_name=_('Cover'),
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name=_('Cover'), null=True, blank=True,
|
||||
)
|
||||
content = RichTextField(
|
||||
_('content'), blank=True, null=True,
|
||||
)
|
||||
date = models.DateTimeField(default=tz.now)
|
||||
featured = models.BooleanField(
|
||||
_('featured'), default=False,
|
||||
)
|
||||
|
@ -86,6 +114,23 @@ class Page(models.Model):
|
|||
def is_trash(self):
|
||||
return self.status == self.STATUS.trash
|
||||
|
||||
@cached_property
|
||||
def headline(self):
|
||||
if not self.content:
|
||||
return ''
|
||||
headline = headline_re.search(self.content)
|
||||
return headline.groupdict()['headline'] if headline else ''
|
||||
|
||||
@classmethod
|
||||
def get_init_kwargs_from(cls, page, **kwargs):
|
||||
kwargs.setdefault('cover', page.cover)
|
||||
kwargs.setdefault('category', page.category)
|
||||
return kwargs
|
||||
|
||||
@classmethod
|
||||
def from_page(cls, page, **kwargs):
|
||||
return cls(**cls.get_init_kwargs_from(page, **kwargs))
|
||||
|
||||
|
||||
class NavItem(models.Model):
|
||||
""" Navigation menu items """
|
||||
|
|
|
@ -470,7 +470,8 @@ class Schedule(BaseRerun):
|
|||
continue
|
||||
|
||||
if initial is None:
|
||||
episode = Episode.from_date(self.program, date)
|
||||
episode = Episode.from_page(self.program, date=date)
|
||||
episode.date = date
|
||||
episodes[date] = episode
|
||||
else:
|
||||
episode = episodes[initial]
|
||||
|
@ -489,10 +490,6 @@ class Schedule(BaseRerun):
|
|||
if self.initial is not None and self.date > self.date:
|
||||
raise ValueError('initial must be later')
|
||||
|
||||
# initial only if it has been yet saved
|
||||
if self.pk:
|
||||
self.__initial = self.__dict__.copy()
|
||||
|
||||
|
||||
class Stream(models.Model):
|
||||
"""
|
||||
|
|
100
aircox/models/signals.py
Executable file
100
aircox/models/signals.py
Executable file
|
@ -0,0 +1,100 @@
|
|||
import pytz
|
||||
|
||||
from django.contrib.auth.models import User, Group, Permission
|
||||
from django.db.models import F, signals
|
||||
from django.dispatch import receiver
|
||||
from django.utils import timezone as tz
|
||||
|
||||
from .. import settings, utils
|
||||
from . import Diffusion, Episode, Program, Schedule
|
||||
|
||||
|
||||
# Add a default group to a user when it is created. It also assigns a list
|
||||
# of permissions to the group if it is created.
|
||||
#
|
||||
# - group name: settings.AIRCOX_DEFAULT_USER_GROUP
|
||||
# - group permissions: settings.AIRCOX_DEFAULT_USER_GROUP_PERMS
|
||||
#
|
||||
@receiver(signals.post_save, sender=User)
|
||||
def user_default_groups(sender, instance, created, *args, **kwargs):
|
||||
"""
|
||||
Set users to different default groups
|
||||
"""
|
||||
if not created or instance.is_superuser:
|
||||
return
|
||||
|
||||
for groupName, permissions in settings.AIRCOX_DEFAULT_USER_GROUPS.items():
|
||||
if instance.groups.filter(name=groupName).count():
|
||||
continue
|
||||
|
||||
group, created = Group.objects.get_or_create(name=groupName)
|
||||
if created and permissions:
|
||||
for codename in permissions:
|
||||
permission = Permission.objects.filter(
|
||||
codename=codename).first()
|
||||
if permission:
|
||||
group.permissions.add(permission)
|
||||
group.save()
|
||||
instance.groups.add(group)
|
||||
|
||||
|
||||
@receiver(signals.post_save, sender=Program)
|
||||
def program_post_save(sender, instance, created, *args, **kwargs):
|
||||
"""
|
||||
Clean-up later diffusions when a program becomes inactive
|
||||
"""
|
||||
if not instance.active:
|
||||
Diffusion.objects.program(instance).after().delete()
|
||||
Episode.object.program(instance).filter(diffusion__isnull=True) \
|
||||
.delete()
|
||||
|
||||
|
||||
@receiver(signals.pre_save, sender=Schedule)
|
||||
def schedule_pre_save(sender, instance, *args, **kwargs):
|
||||
if getattr(instance, 'pk') is not None:
|
||||
instance._initial = Schedule.objects.get(pk=instance.pk)
|
||||
|
||||
|
||||
# TODO
|
||||
@receiver(signals.post_save, sender=Schedule)
|
||||
def schedule_post_save(sender, instance, created, *args, **kwargs):
|
||||
"""
|
||||
Handles Schedule's time, duration and timezone changes and update
|
||||
corresponding diffusions accordingly.
|
||||
"""
|
||||
initial = getattr(instance, '_initial', None)
|
||||
if not initial or ((instance.time, instance.duration, instance.timezone) ==
|
||||
(initial.time, initial.duration, initial.timezone)):
|
||||
return
|
||||
|
||||
today = tz.datetime.today()
|
||||
delta = instance.normalize(today) - initial.normalize(today)
|
||||
|
||||
qs = Diffusion.objects.program(instance.program).after()
|
||||
pks = [d.pk for d in qs if initial.match(d.date)]
|
||||
qs.filter(pk__in=pks).update(
|
||||
start=F('start') + delta,
|
||||
end=F('start') + delta + utils.to_timedelta(instance.duration)
|
||||
)
|
||||
|
||||
|
||||
@receiver(signals.pre_delete, sender=Schedule)
|
||||
def schedule_pre_delete(sender, instance, *args, **kwargs):
|
||||
"""
|
||||
Delete later corresponding diffusion to a changed schedule.
|
||||
"""
|
||||
if not instance.program.sync:
|
||||
return
|
||||
|
||||
qs = Diffusion.objects.program(instance.program).after()
|
||||
pks = [d.pk for d in qs if instance.match(d.date)]
|
||||
qs.filter(pk__in=pks).delete()
|
||||
|
||||
|
||||
@receiver(signals.post_delete, sender=Diffusion)
|
||||
def diffusion_post_delete(sender, instance, *args, **kwargs):
|
||||
Episode.objects.filter(diffusion__isnull=True, content_isnull=True,
|
||||
sound__isnull=True) \
|
||||
.delete()
|
||||
|
||||
|
|
@ -3,9 +3,11 @@ import stat
|
|||
|
||||
from django.conf import settings
|
||||
|
||||
def ensure (key, default):
|
||||
|
||||
def ensure(key, default):
|
||||
globals()[key] = getattr(settings, key, default)
|
||||
|
||||
|
||||
########################################################################
|
||||
# Global & misc
|
||||
########################################################################
|
||||
|
@ -48,7 +50,7 @@ ensure('AIRCOX_EPISODE_TITLE_DATE_FORMAT', '%-d %B %Y')
|
|||
# Directory where to save logs' archives
|
||||
ensure('AIRCOX_LOGS_ARCHIVES_DIR',
|
||||
os.path.join(AIRCOX_DATA_DIR, 'archives')
|
||||
)
|
||||
)
|
||||
# In days, minimal age of a log before it is archived
|
||||
ensure('AIRCOX_LOGS_ARCHIVES_MIN_AGE', 60)
|
||||
|
||||
|
@ -70,21 +72,21 @@ ensure('AIRCOX_SOUND_AUTO_CHMOD', True)
|
|||
# and stat.*
|
||||
ensure(
|
||||
'AIRCOX_SOUND_CHMOD_FLAGS',
|
||||
(stat.S_IRWXU, stat.S_IRWXU | stat.S_IRWXG | stat.S_IROTH )
|
||||
(stat.S_IRWXU, stat.S_IRWXU | stat.S_IRWXG | stat.S_IROTH)
|
||||
)
|
||||
|
||||
# Quality attributes passed to sound_quality_check from sounds_monitor
|
||||
ensure('AIRCOX_SOUND_QUALITY', {
|
||||
'attribute': 'RMS lev dB',
|
||||
'range': (-18.0, -8.0),
|
||||
'sample_length': 120,
|
||||
}
|
||||
'attribute': 'RMS lev dB',
|
||||
'range': (-18.0, -8.0),
|
||||
'sample_length': 120,
|
||||
}
|
||||
)
|
||||
|
||||
# Extension of sound files
|
||||
ensure(
|
||||
'AIRCOX_SOUND_FILE_EXT',
|
||||
('.ogg','.flac','.wav','.mp3','.opus')
|
||||
('.ogg', '.flac', '.wav', '.mp3', '.opus')
|
||||
)
|
||||
|
||||
|
||||
|
@ -107,6 +109,3 @@ ensure(
|
|||
ensure('AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER', ';')
|
||||
# Text delimiter of csv text files
|
||||
ensure('AIRCOX_IMPORT_PLAYLIST_CSV_TEXT_QUOTE', '"')
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,92 +0,0 @@
|
|||
import pytz
|
||||
|
||||
from django.contrib.auth.models import User, Group, Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import F
|
||||
from django.db.models.signals import post_save, pre_save, pre_delete, m2m_changed
|
||||
from django.dispatch import receiver, Signal
|
||||
from django.utils import timezone as tz
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
|
||||
import aircox.models as models
|
||||
import aircox.utils as utils
|
||||
import aircox.settings as settings
|
||||
|
||||
|
||||
|
||||
# Add a default group to a user when it is created. It also assigns a list
|
||||
# of permissions to the group if it is created.
|
||||
#
|
||||
# - group name: settings.AIRCOX_DEFAULT_USER_GROUP
|
||||
# - group permissions: settings.AIRCOX_DEFAULT_USER_GROUP_PERMS
|
||||
#
|
||||
@receiver(post_save, sender=User)
|
||||
def user_default_groups(sender, instance, created, *args, **kwargs):
|
||||
"""
|
||||
Set users to different default groups
|
||||
"""
|
||||
if not created or instance.is_superuser:
|
||||
return
|
||||
|
||||
for groupName, permissions in settings.AIRCOX_DEFAULT_USER_GROUPS.items():
|
||||
if instance.groups.filter(name = groupName).count():
|
||||
continue
|
||||
|
||||
group, created = Group.objects.get_or_create(name = groupName)
|
||||
if created and permissions:
|
||||
for codename in permissions:
|
||||
permission = Permission.objects.filter(codename = codename).first()
|
||||
if permission:
|
||||
group.permissions.add(permission)
|
||||
group.save()
|
||||
instance.groups.add(group)
|
||||
|
||||
@receiver(post_save, sender=models.Program)
|
||||
def program_post_save(sender, instance, created, *args, **kwargs):
|
||||
"""
|
||||
Clean-up later diffusions when a program becomes inactive
|
||||
"""
|
||||
if not instance.active:
|
||||
instance.diffusion_set.after().delete()
|
||||
|
||||
@receiver(post_save, sender=models.Schedule)
|
||||
def schedule_post_save(sender, instance, created, *args, **kwargs):
|
||||
"""
|
||||
Handles Schedule's time, duration and timezone changes and update
|
||||
corresponding diffusions accordingly.
|
||||
"""
|
||||
if created or not instance.program.sync or \
|
||||
not instance.changed(['time','duration','timezone']):
|
||||
return
|
||||
|
||||
initial = instance._Schedule__initial
|
||||
initial = models.Schedule(**{ k: v
|
||||
for k, v in instance._Schedule__initial.items()
|
||||
if not k.startswith('_')
|
||||
})
|
||||
|
||||
today = tz.datetime.today()
|
||||
delta = instance.normalize(today) - \
|
||||
initial.normalize(today)
|
||||
|
||||
qs = models.Diffusion.objects.program(instance.program).after()
|
||||
pks = [ d.pk for d in qs if initial.match(d.date) ]
|
||||
qs.filter(pk__in = pks).update(
|
||||
start = F('start') + delta,
|
||||
end = F('start') + delta + utils.to_timedelta(instance.duration)
|
||||
)
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=models.Schedule)
|
||||
def schedule_pre_delete(sender, instance, *args, **kwargs):
|
||||
"""
|
||||
Delete later corresponding diffusion to a changed schedule.
|
||||
"""
|
||||
if not instance.program.sync:
|
||||
return
|
||||
|
||||
qs = models.Diffusion.objects.program(instance.program).after()
|
||||
pks = [ d.pk for d in qs if instance.match(d.date) ]
|
||||
qs.filter(pk__in = pks).delete()
|
||||
|
||||
|
|
@ -7169,7 +7169,7 @@ label.panel-block {
|
|||
float: right;
|
||||
max-width: 45%; }
|
||||
|
||||
.page > .header {
|
||||
.page .header {
|
||||
margin-bottom: 1.5em; }
|
||||
|
||||
.page .headline {
|
||||
|
@ -7179,6 +7179,11 @@ label.panel-block {
|
|||
.page p {
|
||||
padding: 0.4em 0em; }
|
||||
|
||||
section > .toolbar {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
padding: 1em;
|
||||
margin-bottom: 1.5em; }
|
||||
|
||||
.cover {
|
||||
margin: 1em 0em;
|
||||
border: 0.2em black solid; }
|
||||
|
|
|
@ -5,10 +5,11 @@ Context:
|
|||
{% endcomment %}
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="application-name" content="aircox">
|
||||
<meta name="description" content="{{ site.description }}">
|
||||
<meta name="keywords" content="{{ site.tags }}">
|
||||
<meta charset="utf-8" />
|
||||
<meta name="application-name" content="aircox" />
|
||||
<meta name="description" content="{{ site.description }}" />
|
||||
<meta name="keywords" content="{{ site.tags }}" />
|
||||
<meta name="generator" content="Aircox" />
|
||||
<link rel="icon" href="{% thumbnail site.favicon 32x32 crop %}" />
|
||||
|
||||
{% block assets %}
|
||||
|
@ -18,7 +19,7 @@ Context:
|
|||
{% endblock %}
|
||||
|
||||
<title>
|
||||
{% block head_title %}{{ site.title }}{% endblock %}
|
||||
{% block head_title %}{{ station.name }}{% endblock %}
|
||||
</title>
|
||||
|
||||
{% block head_extra %}{% endblock %}
|
||||
|
@ -52,12 +53,14 @@ Context:
|
|||
{% block header %}
|
||||
<h1 class="title is-1">{% block title %}{% endblock %}</h1>
|
||||
|
||||
{% if parent %}
|
||||
<h4 class="subtitle is-size-3">
|
||||
<a href="{{ parent.get_absolute_url }}">
|
||||
<h4 class="subtitle is-size-3 columns">
|
||||
{% block subtitle %}
|
||||
{% if parent %}
|
||||
<a href="{{ parent.get_absolute_url }}" class="column">
|
||||
❬ {{ parent.title }}</a></li>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</h4>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</header>
|
||||
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
{% load i18n easy_thumbnails_tags aircox %}
|
||||
{% comment %}
|
||||
Context variables:
|
||||
- object: the actual diffusion
|
||||
- page: current parent page in which item is rendered
|
||||
- hide_schedule: if True, do not display start time
|
||||
- hide_headline: if True, do not display headline
|
||||
{% endcomment %}
|
||||
|
||||
{% with object.episode as episode %}
|
||||
{% with episode.program as program %}
|
||||
<article class="media">
|
||||
<div class="media-left">
|
||||
<img src="{% thumbnail episode.cover 128x128 crop=scale %}"
|
||||
class="small-cover">
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<h5 class="subtitle is-size-5">
|
||||
{% if episode.is_published %}
|
||||
<a href="{{ episode.get_absolute_url }}">{{ episode.title }}</a>
|
||||
{% endif %}
|
||||
</h5>
|
||||
|
||||
<div class="">
|
||||
{% if not page or program != page %}
|
||||
{% if program.is_published %}
|
||||
<a href="{{ program.get_absolute_url }}" class="has-text-grey-dark">
|
||||
{{ program.title }}</a>
|
||||
{% else %}{{ program.title }}
|
||||
{% endif %}
|
||||
{% if not hide_schedule %} — {% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if not hide_schedule %}
|
||||
<time datetime="{{ object.start|date:"c" }}" title="{{ object.start }}"
|
||||
class="has-text-weight-light is-size-6">
|
||||
{{ object.start|date:"d M, H:i" }}
|
||||
</time>
|
||||
{% endif %}
|
||||
|
||||
{% if object.initial %}
|
||||
{% with object.initial.date as date %}
|
||||
<span class="tag is-info" title="{% blocktrans %}Rerun of {{ date }}{% endblocktrans %}">
|
||||
{% trans "rerun" %}
|
||||
</span>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% if not hide_headline %}
|
||||
<div class="content">
|
||||
{{ episode.headline }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</article>
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
{% extends "aircox/page.html" %}
|
||||
{% load i18n aircox %}
|
||||
|
||||
{% block title %}
|
||||
{% if program %}
|
||||
{% with program.name as program %}
|
||||
{% blocktrans %}Diffusions of {{ program }}{% endblocktrans %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% trans "All diffusions" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<section>
|
||||
{% for object in object_list %}
|
||||
{% include "aircox/diffusion_item.html" %}
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
||||
|
||||
{% if is_paginated %}
|
||||
<nav class="pagination is-centered" role="pagination" aria-label="{% trans "pagination" %}">
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="?page={{ page_obj.previous_page_number }}" class="pagination-previous">
|
||||
{% else %}
|
||||
<a class="pagination-previous" disabled>
|
||||
{% endif %}
|
||||
{% trans "Previous" %}</a>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a href="?page={{ page_obj.next_page_number }}" class="pagination-next">
|
||||
{% else %}
|
||||
<a class="pagination-next" disabled>
|
||||
{% endif %}
|
||||
{% trans "Next" %}</a>
|
||||
|
||||
<ul class="pagination-list">
|
||||
{% for i in paginator.page_range %}
|
||||
<li>
|
||||
<a class="pagination-link {% if page_obj.number == i %}is-current{% endif %}"
|
||||
href="?page={{ i }}">{{ i }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
@ -34,15 +34,17 @@
|
|||
{% for day, diffusions in by_date.items %}
|
||||
<noscript><h4 class="subtitle is-4">{{ day|date:"l d F Y" }}</h4></noscript>
|
||||
<div id="{{timetable_id}}-{{ day|date:"Y-m-d" }}" v-if="value == '{{ day }}'">
|
||||
{% for object in diffusions %}
|
||||
{% for diffusion in diffusions %}
|
||||
<div class="columns">
|
||||
<div class="column is-one-fifth has-text-right">
|
||||
<time datetime="{{ object.start|date:"c" }}">
|
||||
{{ object.start|date:"H:i" }} - {{ object.end|date:"H:i" }}
|
||||
<time datetime="{{ diffusion.start|date:"c" }}">
|
||||
{{ diffusion.start|date:"H:i" }} - {{ diffusion.end|date:"H:i" }}
|
||||
</time>
|
||||
</div>
|
||||
<div class="column">
|
||||
{% include "aircox/diffusion_item.html" %}
|
||||
{% with diffusion.episode as object %}
|
||||
{% include "aircox/episode_item.html" %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
|
|
@ -1,6 +1,33 @@
|
|||
{% extends "aircox/program_base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block header %}
|
||||
{{ block.super }}
|
||||
|
||||
<section class="is-size-5 has-text-weight-bold">
|
||||
{% for diffusion in object.diffusion_set.all %}
|
||||
{% with diffusion.start as start %}
|
||||
{% with diffusion.end as end %}
|
||||
<time datetime="{{ start }}">{{ start|date:"D. d F Y, H:i" }}</time>
|
||||
—
|
||||
<time datetime="{{ end }}">{{ end|date:"H:i" }}</time>
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
|
||||
<small>
|
||||
{% if diffusion.initial %}
|
||||
{% with diffusion.initial.date as date %}
|
||||
<span title="{% blocktrans %}Rerun of {{ date }}{% endblocktrans %}">
|
||||
({% trans "rerun" %})
|
||||
</span>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</small>
|
||||
<br>
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{{ block.super }}
|
||||
|
||||
|
|
|
@ -39,7 +39,11 @@
|
|||
{{ object.start|date:"H:i" }} - {{ object.end|date:"H:i" }}
|
||||
</time>
|
||||
</td>
|
||||
<td>{% include "aircox/diffusion_item.html" %}</td>
|
||||
{% with object as diffusion %}
|
||||
{% with diffusion.episode as object %}
|
||||
<td>{% include "aircox/episode_item.html" %}</td>
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<td>
|
||||
<time datetime="{{ object.date }}" title="{{ object.date }}">
|
||||
|
|
|
@ -7,10 +7,17 @@ Context:
|
|||
- page: page
|
||||
{% endcomment %}
|
||||
|
||||
{% block subtitle %}
|
||||
{{ block.super }}
|
||||
{% if page.category %}
|
||||
<span class="column has-text-right">{{ page.category.title }}</span>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block head_title %}
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
{% if title %} — {% endif %}
|
||||
{{ site.title }}
|
||||
{% if title %} ‐ {% endif %}
|
||||
{{ station.name }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div class="podcast">
|
||||
{% if object.embed %}
|
||||
{{ object.embed }}
|
||||
{{ object.embed|safe }}
|
||||
{% else %}
|
||||
<audio src="{{ object.url }}" controls>
|
||||
{% endif %}
|
||||
|
|
|
@ -4,22 +4,22 @@
|
|||
{% block side_nav %}
|
||||
{{ block.super }}
|
||||
|
||||
{% if diffusions %}
|
||||
{% if episodes %}
|
||||
<section>
|
||||
<h4 class="subtitle is-size-4">{% trans "Last shows" %}</h4>
|
||||
|
||||
{% for object in diffusions %}
|
||||
{% include "aircox/diffusion_item.html" %}
|
||||
{% for object in episodes %}
|
||||
{% include "aircox/episode_item.html" %}
|
||||
{% endfor %}
|
||||
|
||||
<br>
|
||||
<nav class="pagination is-centered">
|
||||
<ul class="pagination-list">
|
||||
<li>
|
||||
<a href="{% url "diffusion-list" program_slug=page.slug %}"
|
||||
<a href="{% url "diffusion-list" program_slug=program.slug %}"
|
||||
class="pagination-link"
|
||||
aria-label="{% trans "Show all diffusions" %}">
|
||||
{% trans "All diffusions" %}
|
||||
{% trans "All shows" %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
@ -1,25 +1,24 @@
|
|||
{% load i18n %}
|
||||
<section class="is-size-5">
|
||||
<section class="is-size-5 has-text-weight-bold">
|
||||
{% for schedule in program.schedule_set.all %}
|
||||
<p>
|
||||
{{ schedule.get_frequency_verbose }}
|
||||
{% with schedule.start|date:"H:i" as start %}
|
||||
{% with schedule.end|date:"H:i" as end %}
|
||||
<time datetime="{{ start }}">{{ start }}</time>
|
||||
—
|
||||
<time datetime="{{ end }}">{{ end }}</time>
|
||||
{{ schedule.get_frequency_verbose }}
|
||||
{% with schedule.start|date:"H:i" as start %}
|
||||
{% with schedule.end|date:"H:i" as end %}
|
||||
<time datetime="{{ start }}">{{ start }}</time>
|
||||
—
|
||||
<time datetime="{{ end }}">{{ end }}</time>
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
<small>
|
||||
{% if schedule.initial %}
|
||||
{% with schedule.initial.date as date %}
|
||||
<span title="{% blocktrans %}Rerun of {{ date }}{% endblocktrans %}">
|
||||
({% trans "rerun" %})
|
||||
</span>
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
<small>
|
||||
{% if schedule.initial %}
|
||||
{% with schedule.initial.date as date %}
|
||||
<span title="{% blocktrans %}Rerun of {{ date }}{% endblocktrans %}">
|
||||
({% trans "rerun" %})
|
||||
</span>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</small>
|
||||
</p>
|
||||
{% endif %}
|
||||
</small>
|
||||
<br>
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
||||
|
|
|
@ -8,17 +8,33 @@ random.seed()
|
|||
register = template.Library()
|
||||
|
||||
|
||||
@register.simple_tag(name='unique_id')
|
||||
def do_unique_id(prefix=''):
|
||||
value = str(random.random()).replace('.', '')
|
||||
return prefix + '_' + value if prefix else value
|
||||
@register.filter(name='verbose_name')
|
||||
def do_verbose_name(obj, plural=False):
|
||||
""" Return model's verbose name (singular or plural) """
|
||||
return obj._meta.verbose_name_plural if plural else \
|
||||
obj._meta.verbose_name
|
||||
|
||||
@register.simple_tag(name='update_query')
|
||||
def do_update_query(obj, **kwargs):
|
||||
""" Replace provided querydict's values with **kwargs. """
|
||||
for k, v in kwargs.items():
|
||||
if v is not None:
|
||||
obj[k] = list(v) if hasattr(v, '__iter__') else [v]
|
||||
elif k in obj:
|
||||
obj.pop(k)
|
||||
return obj
|
||||
|
||||
@register.filter(name='is_diffusion')
|
||||
def do_is_diffusion(obj):
|
||||
return isinstance(obj, Diffusion)
|
||||
|
||||
|
||||
@register.simple_tag(name='unique_id')
|
||||
def do_unique_id(prefix=''):
|
||||
value = str(random.random()).replace('.', '')
|
||||
return prefix + '_' + value if prefix else value
|
||||
|
||||
|
||||
@register.simple_tag(name='nav_items', takes_context=True)
|
||||
def do_nav_items(context, menu, **kwargs):
|
||||
station, request = context['station'], context['request']
|
||||
|
|
|
@ -11,20 +11,22 @@ from aircox.models import *
|
|||
logger = logging.getLogger('aircox.test')
|
||||
logger.setLevel('INFO')
|
||||
|
||||
|
||||
class ScheduleCheck (TestCase):
|
||||
def setUp(self):
|
||||
self.schedules = [
|
||||
Schedule(
|
||||
date = tz.now(),
|
||||
duration = datetime.time(1,30),
|
||||
frequency = frequency,
|
||||
date=tz.now(),
|
||||
duration=datetime.time(1, 30),
|
||||
frequency=frequency,
|
||||
)
|
||||
for frequency in Schedule.Frequency.__members__.values()
|
||||
]
|
||||
|
||||
def test_frequencies(self):
|
||||
for schedule in self.schedules:
|
||||
logger.info('- test frequency %s' % schedule.get_frequency_display())
|
||||
logger.info('- test frequency %s' %
|
||||
schedule.get_frequency_display())
|
||||
date = schedule.date
|
||||
count = 24
|
||||
while count:
|
||||
|
@ -40,7 +42,7 @@ class ScheduleCheck (TestCase):
|
|||
self.check_last(schedule, date, dates)
|
||||
else:
|
||||
pass
|
||||
date += relativedelta(months = 1)
|
||||
date += relativedelta(months=1)
|
||||
|
||||
def check_one_on_two(self, schedule, date, dates):
|
||||
for date in dates:
|
||||
|
@ -53,14 +55,11 @@ class ScheduleCheck (TestCase):
|
|||
|
||||
# end of month before the wanted weekday: move one week back
|
||||
if date.weekday() < schedule.date.weekday():
|
||||
date -= datetime.timedelta(days = 7)
|
||||
date -= datetime.timedelta(days=7)
|
||||
|
||||
date -= datetime.timedelta(days = date.weekday())
|
||||
date += datetime.timedelta(days = schedule.date.weekday())
|
||||
date -= datetime.timedelta(days=date.weekday())
|
||||
date += datetime.timedelta(days=schedule.date.weekday())
|
||||
self.assertEqual(date, dates[0].date())
|
||||
|
||||
def check_n_of_week(self, schedule, date, dates):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from django.urls import path, register_converter
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from . import views
|
||||
from . import views, models
|
||||
from .converters import PagePathConverter, DateConverter, WeekConverter
|
||||
|
||||
|
||||
|
@ -10,21 +10,32 @@ register_converter(DateConverter, 'date')
|
|||
register_converter(WeekConverter, 'week')
|
||||
|
||||
|
||||
#urls = [
|
||||
# urls = [
|
||||
# path('on_air', views.on_air, name='aircox.on_air'),
|
||||
# path('monitor', views.Monitor.as_view(), name='aircox.monitor'),
|
||||
# path('stats', views.StatisticsView.as_view(), name='aircox.stats'),
|
||||
#]
|
||||
# ]
|
||||
|
||||
|
||||
urls = [
|
||||
# path('', views.PageDetailView.as_view(model=models.Article),
|
||||
# name='home'),
|
||||
path(_('articles/'),
|
||||
views.ArticleListView.as_view(model=models.Article, is_static=False),
|
||||
name='article-list'),
|
||||
path(_('articles/<slug:slug>/'),
|
||||
views.PageDetailView.as_view(model=models.Article),
|
||||
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/'),
|
||||
views.DiffusionListView.as_view(), name='diffusion-list'),
|
||||
views.EpisodeListView.as_view(), name='diffusion-list'),
|
||||
|
||||
path(_('episodes/'),
|
||||
views.DiffusionListView.as_view(), name='diffusion-list'),
|
||||
views.EpisodeListView.as_view(), name='diffusion-list'),
|
||||
path(_('episodes/week/'),
|
||||
views.TimetableView.as_view(), name='timetable'),
|
||||
path(_('episodes/week/<week:date>/'),
|
||||
|
@ -36,5 +47,3 @@ urls = [
|
|||
path(_('logs/<date:date>/'), views.LogListView.as_view(), name='logs'),
|
||||
# path('<page_path:path>', views.route_page, name='page'),
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ __all__ = ['Redirect', 'redirect', 'date_range', 'cast_date',
|
|||
|
||||
class Redirect(Exception):
|
||||
""" Redirect exception -- see `redirect()`. """
|
||||
|
||||
def __init__(self, url):
|
||||
self.url = url
|
||||
|
||||
|
@ -82,4 +83,3 @@ def seconds_to_time(seconds):
|
|||
minutes, seconds = divmod(seconds, 60)
|
||||
hours, minutes = divmod(minutes, 60)
|
||||
return datetime.time(hour=hours, minute=minutes, second=seconds)
|
||||
|
||||
|
|
|
@ -2,17 +2,15 @@ import os
|
|||
import json
|
||||
import datetime
|
||||
|
||||
from django.db.models import Count
|
||||
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 _, ugettext_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils import timezone as tz
|
||||
from django.views.decorators.cache import never_cache, cache_page
|
||||
from django.views.decorators.cache import cache_page
|
||||
|
||||
import aircox.models as models
|
||||
import aircox.settings as settings
|
||||
|
||||
|
||||
# FIXME usefull?
|
||||
|
@ -29,6 +27,7 @@ class Stations:
|
|||
for station in self.stations:
|
||||
station.streamer.fetch()
|
||||
|
||||
|
||||
stations = Stations()
|
||||
|
||||
|
||||
|
@ -102,7 +101,7 @@ class Monitor(View, TemplateResponseMixin, LoginRequiredMixin):
|
|||
return HttpResponse('')
|
||||
|
||||
POST = request.POST
|
||||
controller = POST.get('controller')
|
||||
POST.get('controller')
|
||||
action = POST.get('action')
|
||||
|
||||
station = stations.stations.filter(name=POST.get('station')) \
|
||||
|
@ -256,5 +255,3 @@ class StatisticsView(View, TemplateResponseMixin, LoginRequiredMixin):
|
|||
self.request = request
|
||||
context = self.get_context_data(**kwargs)
|
||||
return render(request, self.template_name, context)
|
||||
|
||||
|
||||
|
|
7
aircox/views/__init__.py
Normal file
7
aircox/views/__init__.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
from .article import ArticleListView
|
||||
from .base import BaseView
|
||||
from .episode import EpisodeDetailView, EpisodeListView, TimetableView
|
||||
from .log import LogListView
|
||||
from .page import PageDetailView, PageListView
|
||||
from .program import ProgramDetailView
|
||||
|
14
aircox/views/article.py
Normal file
14
aircox/views/article.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
from ..models import Article
|
||||
from .page import PageListView
|
||||
|
||||
|
||||
__all__ = ['ArticleListView']
|
||||
|
||||
|
||||
class ArticleListView(PageListView):
|
||||
model = Article
|
||||
is_static = False
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset(is_static=self.is_static)
|
||||
|
32
aircox/views/base.py
Normal file
32
aircox/views/base.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
|
||||
from django.http import Http404
|
||||
from django.views.generic import DetailView, ListView
|
||||
from django.views.generic.base import TemplateResponseMixin, ContextMixin
|
||||
|
||||
from ..utils import Redirect
|
||||
|
||||
|
||||
__all__ = ['BaseView', 'PageView']
|
||||
|
||||
|
||||
class BaseView(TemplateResponseMixin, ContextMixin):
|
||||
show_side_nav = False
|
||||
""" Show side navigation """
|
||||
title = None
|
||||
""" Page title """
|
||||
cover = None
|
||||
""" Page cover """
|
||||
|
||||
@property
|
||||
def station(self):
|
||||
return self.request.station
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().station(self.station)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs.setdefault('station', self.station)
|
||||
kwargs.setdefault('cover', self.cover)
|
||||
kwargs.setdefault('show_side_nav', self.show_side_nav)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
99
aircox/views/episode.py
Normal file
99
aircox/views/episode.py
Normal file
|
@ -0,0 +1,99 @@
|
|||
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 .base import BaseView
|
||||
from .page import PageListView
|
||||
from .program import ProgramPageDetailView
|
||||
|
||||
|
||||
__all__ = ['EpisodeDetailView', 'DiffusionListView', 'TimetableView']
|
||||
|
||||
|
||||
class EpisodeDetailView(ProgramPageDetailView):
|
||||
model = Episode
|
||||
|
||||
def get_podcasts(self, diffusion):
|
||||
return Sound.objects.diffusion(diffusion).podcasts()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs.setdefault('program', self.object.program)
|
||||
kwargs.setdefault('parent', kwargs['program'])
|
||||
if not 'podcasts' in kwargs:
|
||||
kwargs['podcasts'] = self.object.sound_set.podcasts()
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
# TODO: pagination: in template, only a limited number of pages displayed
|
||||
class EpisodeListView(PageListView):
|
||||
model = Episode
|
||||
item_template_name = 'aircox/episode_item.html'
|
||||
show_headline = True
|
||||
template_name = 'aircox/diffusion_list.html'
|
||||
program = None
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
program_slug = kwargs.get('program_slug')
|
||||
if program_slug:
|
||||
self.program = get_object_or_404(Program, slug=program_slug)
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset()
|
||||
if self.program:
|
||||
qs = qs.filter(program=self.program)
|
||||
return qs
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class TimetableView(BaseView, ListView):
|
||||
""" View for timetables """
|
||||
template_name_suffix = '_timetable'
|
||||
model = Diffusion
|
||||
# ordering = ('start',)
|
||||
|
||||
date = None
|
||||
start = None
|
||||
end = None
|
||||
|
||||
def get_queryset(self):
|
||||
self.date = self.kwargs.get('date') or datetime.date.today()
|
||||
self.start = self.date - datetime.timedelta(days=self.date.weekday())
|
||||
self.end = self.start + datetime.timedelta(days=7)
|
||||
return super().get_queryset().range(self.start, self.end) \
|
||||
.order_by('start')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
# regoup by dates
|
||||
by_date = OrderedDict()
|
||||
date = self.start
|
||||
while date < self.end:
|
||||
by_date[date] = []
|
||||
date += datetime.timedelta(days=1)
|
||||
|
||||
for diffusion in self.object_list:
|
||||
if diffusion.date not in by_date:
|
||||
continue
|
||||
by_date[diffusion.date].append(diffusion)
|
||||
|
||||
return super().get_context_data(
|
||||
by_date=by_date,
|
||||
date=self.date,
|
||||
start=self.start,
|
||||
end=self.end - datetime.timedelta(days=1),
|
||||
prev_date=self.start - datetime.timedelta(days=1),
|
||||
next_date=self.end + datetime.timedelta(days=1),
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
96
aircox/views/log.py
Normal file
96
aircox/views/log.py
Normal file
|
@ -0,0 +1,96 @@
|
|||
from collections import deque
|
||||
import datetime
|
||||
|
||||
from django.views.generic import ListView
|
||||
|
||||
from ..models import Diffusion, Log
|
||||
from .base import BaseView
|
||||
|
||||
|
||||
__all__ = ['BaseLogView', 'LogListView']
|
||||
|
||||
|
||||
class BaseLogView(ListView):
|
||||
station = None
|
||||
date = None
|
||||
delta = None
|
||||
|
||||
def get_queryset(self):
|
||||
# only get logs for tracks: log for diffusion will be retrieved
|
||||
# by the diffusions' queryset.
|
||||
return super().get_queryset().station(self.station).on_air() \
|
||||
.at(self.date).filter(track__isnull=False)
|
||||
|
||||
def get_diffusions_queryset(self):
|
||||
return Diffusion.objects.station(self.station).on_air() \
|
||||
.today(self.date)
|
||||
|
||||
def get_object_list(self, queryset):
|
||||
diffs = deque(self.get_diffusions_queryset().order_by('start'))
|
||||
logs = list(queryset.order_by('date'))
|
||||
if not len(diffs):
|
||||
return logs
|
||||
|
||||
object_list = []
|
||||
diff = None
|
||||
last_collision = None
|
||||
|
||||
# TODO/FIXME: multiple diffs at once - recheck the whole algorithm in
|
||||
# detail -- however I barely see cases except when there are diff
|
||||
# collision or the streamer is not working
|
||||
for index, log in enumerate(logs):
|
||||
# get next diff
|
||||
if diff is None or diff.end < log.date:
|
||||
diff = diffs.popleft() if len(diffs) else None
|
||||
|
||||
# no more diff that can collide: return list
|
||||
if diff is None:
|
||||
if last_collision and not object_list or \
|
||||
object_list[-1] is not last_collision:
|
||||
object_list.append(last_collision)
|
||||
return object_list + logs[index:]
|
||||
|
||||
# diff colliding with log
|
||||
if diff.start <= log.date:
|
||||
if not object_list or object_list[-1] is not diff:
|
||||
object_list.append(diff)
|
||||
if log.date <= diff.end:
|
||||
last_collision = log
|
||||
else:
|
||||
# add last colliding log: track
|
||||
if last_collision is not None:
|
||||
object_list.append(last_collision)
|
||||
|
||||
object_list.append(log)
|
||||
last_collision = None
|
||||
return object_list
|
||||
|
||||
|
||||
class LogListView(BaseView, BaseLogView):
|
||||
model = Log
|
||||
|
||||
date = None
|
||||
max_age = 10
|
||||
min_date = None
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
today = datetime.date.today()
|
||||
self.min_date = today - datetime.timedelta(days=self.max_age)
|
||||
self.date = min(max(self.min_date, self.kwargs['date']), today) \
|
||||
if 'date' in self.kwargs else today
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
today = datetime.date.today()
|
||||
max_date = min(max(self.date + datetime.timedelta(days=3),
|
||||
self.min_date + datetime.timedelta(days=6)), today)
|
||||
|
||||
return super().get_context_data(
|
||||
date=self.date,
|
||||
min_date=self.min_date,
|
||||
dates=(date for date in (
|
||||
max_date - datetime.timedelta(days=i)
|
||||
for i in range(0, 7)) if date >= self.min_date),
|
||||
object_list=self.get_object_list(self.object_list),
|
||||
**kwargs
|
||||
)
|
84
aircox/views/page.py
Normal file
84
aircox/views/page.py
Normal file
|
@ -0,0 +1,84 @@
|
|||
|
||||
from django.http import Http404
|
||||
from django.views.generic import DetailView, ListView
|
||||
|
||||
from ..models import Category
|
||||
from ..utils import Redirect
|
||||
from .base import BaseView
|
||||
|
||||
|
||||
__all__ = ['PageDetailView', 'PageListView']
|
||||
|
||||
|
||||
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):
|
||||
#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)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class PageListView(BaseView, ListView):
|
||||
template_name = 'aircox/page_list.html'
|
||||
item_template_name = 'aircox/page_item.html'
|
||||
paginate_by = 10
|
||||
show_headline = True
|
||||
show_side_nav = True
|
||||
categories = None
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
self.categories = set(self.request.GET.getlist('categories'))
|
||||
return super().get(*args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset().published() \
|
||||
.select_related('cover', 'category')
|
||||
|
||||
# category can be filtered based on request.GET['categories']
|
||||
# (by id)
|
||||
if self.categories:
|
||||
qs = qs.filter(category__slug__in=self.categories)
|
||||
return qs.order_by('-date')
|
||||
|
||||
def get_categories_queryset(self):
|
||||
# TODO: use generic reverse field lookup
|
||||
categories = self.model.objects.published() \
|
||||
.filter(category__isnull=False) \
|
||||
.values_list('category', flat=True)
|
||||
return Category.objects.filter(id__in=categories)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs.setdefault('item_template_name', self.item_template_name)
|
||||
kwargs.setdefault('filter_categories', self.get_categories_queryset())
|
||||
kwargs.setdefault('categories', self.categories)
|
||||
kwargs.setdefault('show_headline', self.show_headline)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
29
aircox/views/program.py
Normal file
29
aircox/views/program.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
|
||||
from aircox.models import Episode, Program
|
||||
from .page import PageDetailView
|
||||
|
||||
|
||||
__all__ = ['ProgramPageDetailView', 'ProgramDetailView']
|
||||
|
||||
|
||||
class ProgramPageDetailView(PageDetailView):
|
||||
""" Base view class for rendering content of a specific programs. """
|
||||
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 ProgramDetailView(ProgramPageDetailView):
|
||||
model = Program
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs.setdefault('program', self.object)
|
||||
return super().get_context_data(**kwargs)
|
|
@ -33,7 +33,7 @@ $body-background-color: $light;
|
|||
max-width: 45%;
|
||||
}
|
||||
|
||||
& > .header {
|
||||
.header {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
|
@ -47,6 +47,12 @@ $body-background-color: $light;
|
|||
}
|
||||
}
|
||||
|
||||
section > .toolbar {
|
||||
background-color: rgba(0,0,0,0.05);
|
||||
padding: 1em;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
|
||||
.cover {
|
||||
margin: 1em 0em;
|
||||
|
|
Loading…
Reference in New Issue
Block a user