website
This commit is contained in:
parent
324cf2ef0f
commit
248b77fca4
|
@ -1,3 +1,2 @@
|
||||||
|
|
||||||
default_app_config = 'aircox.apps.AircoxConfig'
|
default_app_config = 'aircox.apps.AircoxConfig'
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
|
from .article import ArticleAdmin
|
||||||
from .episode import DiffusionAdmin, EpisodeAdmin
|
from .episode import DiffusionAdmin, EpisodeAdmin
|
||||||
from .log import LogAdmin
|
from .log import LogAdmin
|
||||||
# from .playlist import PlaylistAdmin
|
|
||||||
from .program import ProgramAdmin, ScheduleAdmin, StreamAdmin
|
from .program import ProgramAdmin, ScheduleAdmin, StreamAdmin
|
||||||
from .sound import SoundAdmin
|
from .sound import SoundAdmin, TrackAdmin
|
||||||
from .station import StationAdmin
|
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 aircox.models import Episode, Diffusion, Sound, Track
|
||||||
|
|
||||||
from .page import PageAdmin
|
from .page import PageAdmin
|
||||||
from .playlist import TracksInline
|
from .sound import SoundInline, TracksInline
|
||||||
from .sound import SoundInline
|
|
||||||
|
|
||||||
|
|
||||||
class DiffusionBaseAdmin:
|
class DiffusionBaseAdmin:
|
||||||
|
|
|
@ -4,18 +4,30 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from adminsortable2.admin import SortableInlineAdminMixin
|
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):
|
class PageAdmin(admin.ModelAdmin):
|
||||||
list_display = ('cover_thumb', 'title', 'status')
|
list_display = ('cover_thumb', 'title', 'status', 'category')
|
||||||
list_display_links = ('cover_thumb', 'title')
|
list_display_links = ('cover_thumb', 'title')
|
||||||
list_editable = ('status',)
|
list_editable = ('status', 'category')
|
||||||
prepopulated_fields = {"slug": ("title",)}
|
prepopulated_fields = {"slug": ("title",)}
|
||||||
|
|
||||||
fieldsets = [
|
fieldsets = [
|
||||||
('', {
|
('', {
|
||||||
'fields': ['title', 'slug', 'cover', 'content'],
|
'fields': ['title', 'slug', 'category', 'cover', 'content'],
|
||||||
}),
|
}),
|
||||||
(_('Publication Settings'), {
|
(_('Publication Settings'), {
|
||||||
'fields': ['featured', 'allow_comments', 'status'],
|
'fields': ['featured', 'allow_comments', 'status'],
|
||||||
|
@ -31,3 +43,5 @@ class PageAdmin(admin.ModelAdmin):
|
||||||
class NavItemInline(SortableInlineAdminMixin, admin.TabularInline):
|
class NavItemInline(SortableInlineAdminMixin, admin.TabularInline):
|
||||||
model = NavItem
|
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.contrib import admin
|
||||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||||
|
|
||||||
from aircox.models import Sound
|
from adminsortable2.admin import SortableInlineAdminMixin
|
||||||
from .playlist import TracksInline
|
|
||||||
|
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):
|
class SoundInline(admin.TabularInline):
|
||||||
|
@ -31,4 +43,19 @@ class SoundAdmin(admin.ModelAdmin):
|
||||||
inlines = [TracksInline]
|
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'
|
verbose_name = 'Aircox'
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
import aircox.signals
|
pass
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ class Connector:
|
||||||
return
|
return
|
||||||
|
|
||||||
family = socket.AF_UNIX if isinstance(self.address, str) else \
|
family = socket.AF_UNIX if isinstance(self.address, str) else \
|
||||||
socket.AF_INET
|
socket.AF_INET
|
||||||
try:
|
try:
|
||||||
self.socket = socket.socket(family, socket.SOCK_STREAM)
|
self.socket = socket.socket(family, socket.SOCK_STREAM)
|
||||||
self.socket.connect(self.address)
|
self.socket.connect(self.address)
|
||||||
|
@ -81,4 +81,3 @@ class Connector:
|
||||||
return json.loads(value) if value else None
|
return json.loads(value) if value else None
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
from collections import OrderedDict
|
|
||||||
import atexit
|
import atexit
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
@ -121,9 +120,6 @@ class Streamer:
|
||||||
|
|
||||||
def sync(self):
|
def sync(self):
|
||||||
""" Sync all sources. """
|
""" Sync all sources. """
|
||||||
if self.process is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
for source in self.sources:
|
for source in self.sources:
|
||||||
source.sync()
|
source.sync()
|
||||||
|
|
||||||
|
@ -141,7 +137,7 @@ class Streamer:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.source = next((source for source in self.sources
|
self.source = next((source for source in self.sources
|
||||||
if source.is_playing), None)
|
if source.is_playing), None)
|
||||||
|
|
||||||
# Process ##########################################################
|
# Process ##########################################################
|
||||||
def get_process_args(self):
|
def get_process_args(self):
|
||||||
|
@ -214,8 +210,8 @@ class Source:
|
||||||
def is_playing(self):
|
def is_playing(self):
|
||||||
return self.status == 'playing'
|
return self.status == 'playing'
|
||||||
|
|
||||||
#@property
|
# @property
|
||||||
#def is_on_air(self):
|
# def is_on_air(self):
|
||||||
# return self.rid is not None and self.rid in self.controller.on_air
|
# return self.rid is not None and self.rid in self.controller.on_air
|
||||||
|
|
||||||
def __init__(self, controller, id=None):
|
def __init__(self, controller, id=None):
|
||||||
|
@ -224,7 +220,6 @@ class Source:
|
||||||
|
|
||||||
def sync(self):
|
def sync(self):
|
||||||
""" Synchronize what should be synchronized """
|
""" Synchronize what should be synchronized """
|
||||||
pass
|
|
||||||
|
|
||||||
def fetch(self):
|
def fetch(self):
|
||||||
data = self.controller.send(self.id, '.remaining')
|
data = self.controller.send(self.id, '.remaining')
|
||||||
|
@ -322,5 +317,3 @@ class QueueSource(Source):
|
||||||
super().fetch()
|
super().fetch()
|
||||||
queue = self.controller.send(self.id, '_queue.queue').split(' ')
|
queue = self.controller.send(self.id, '_queue.queue').split(' ')
|
||||||
self.queue = queue
|
self.queue = queue
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -45,4 +45,3 @@ class DateConverter:
|
||||||
def to_url(self, value):
|
def to_url(self, value):
|
||||||
return '{:04d}/{:02d}/{:02d}'.format(value.year, value.month,
|
return '{:04d}/{:02d}/{:02d}'.format(value.year, value.month,
|
||||||
value.day)
|
value.day)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import pytz
|
import pytz
|
||||||
from django import shortcuts
|
from django.db.models import Q
|
||||||
from django.db.models import Q, Case, Value, When
|
|
||||||
from django.utils import timezone as tz
|
from django.utils import timezone as tz
|
||||||
|
|
||||||
from .models import Station
|
from .models import Station
|
||||||
|
@ -16,18 +15,18 @@ class AircoxMiddleware(object):
|
||||||
This middleware must be set after the middleware
|
This middleware must be set after the middleware
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, get_response):
|
def __init__(self, get_response):
|
||||||
self.get_response = get_response
|
self.get_response = get_response
|
||||||
|
|
||||||
def get_station(self, request):
|
def get_station(self, request):
|
||||||
""" Return station for the provided request """
|
""" Return station for the provided request """
|
||||||
expr = Q(default=True) | Q(hosts__contains=request.get_host())
|
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)))
|
# When(default=True, then=Value(32)))
|
||||||
return Station.objects.filter(expr).order_by('default').first()
|
return Station.objects.filter(expr).order_by('default').first()
|
||||||
# .annotate(resolve_priority=case) \
|
# .annotate(resolve_priority=case) \
|
||||||
#.order_by('resolve_priority').first()
|
# .order_by('resolve_priority').first()
|
||||||
|
|
||||||
|
|
||||||
def init_timezone(self, request):
|
def init_timezone(self, request):
|
||||||
# note: later we can use http://freegeoip.net/ on user side if
|
# note: later we can use http://freegeoip.net/ on user side if
|
||||||
|
@ -44,13 +43,10 @@ class AircoxMiddleware(object):
|
||||||
timezone = tz.get_current_timezone()
|
timezone = tz.get_current_timezone()
|
||||||
tz.activate(timezone)
|
tz.activate(timezone)
|
||||||
|
|
||||||
|
|
||||||
def __call__(self, request):
|
def __call__(self, request):
|
||||||
self.init_timezone(request)
|
self.init_timezone(request)
|
||||||
request.station = self.get_station(request)
|
request.station = self.get_station(request)
|
||||||
try:
|
try:
|
||||||
return self.get_response(request)
|
return self.get_response(request)
|
||||||
except Redirect as redirect:
|
except Redirect:
|
||||||
return
|
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 .program import Program, Stream, Schedule
|
||||||
from .episode import Episode, Diffusion
|
from .episode import Episode, Diffusion
|
||||||
from .log import Log
|
from .log import Log
|
||||||
from .sound import Sound, Track
|
from .sound import Sound, Track
|
||||||
from .station import Station, Port
|
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']
|
__all__ = ['Episode', 'Diffusion', 'DiffusionQuerySet']
|
||||||
|
|
||||||
|
|
||||||
|
class EpisodeQuerySet(PageQuerySet, InProgramQuerySet):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Episode(Page):
|
class Episode(Page):
|
||||||
program = models.ForeignKey(
|
program = models.ForeignKey(
|
||||||
Program, models.CASCADE,
|
Program, models.CASCADE,
|
||||||
verbose_name=_('program'),
|
verbose_name=_('program'),
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = InProgramQuerySet.as_manager()
|
objects = EpisodeQuerySet.as_manager()
|
||||||
detail_url_name = 'episode-detail'
|
detail_url_name = 'episode-detail'
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -37,17 +41,14 @@ class Episode(Page):
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_default_title(cls, program, date):
|
def get_init_kwargs_from(cls, page, date, title=None, **kwargs):
|
||||||
""" Get default Episode's title """
|
""" Get default Episode's title """
|
||||||
return settings.AIRCOX_EPISODE_TITLE.format(
|
title = settings.AIRCOX_EPISODE_TITLE.format(
|
||||||
program=program,
|
program=page,
|
||||||
date=date.strftime(settings.AIRCOX_EPISODE_TITLE_DATE_FORMAT),
|
date=date.strftime(settings.AIRCOX_EPISODE_TITLE_DATE_FORMAT),
|
||||||
)
|
) if title is None else title
|
||||||
|
return super().get_init_kwargs_from(page, title=title, program=page,
|
||||||
@classmethod
|
**kwargs)
|
||||||
def from_date(cls, program, date):
|
|
||||||
title = cls.get_default_title(program, date)
|
|
||||||
return cls(program=program, title=title, cover=program.cover)
|
|
||||||
|
|
||||||
|
|
||||||
class DiffusionQuerySet(BaseRerunQuerySet):
|
class DiffusionQuerySet(BaseRerunQuerySet):
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
import re
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone as tz
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.utils.functional import cached_property
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
|
|
||||||
from ckeditor.fields import RichTextField
|
from ckeditor.fields import RichTextField
|
||||||
from filer.fields.image import FilerImageField
|
from filer.fields.image import FilerImageField
|
||||||
|
@ -16,13 +16,36 @@ from model_utils.managers import InheritanceQuerySet
|
||||||
from .station import Station
|
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):
|
class PageQuerySet(InheritanceQuerySet):
|
||||||
|
def draft(self):
|
||||||
|
return self.filter(status=Page.STATUS.draft)
|
||||||
|
|
||||||
def published(self):
|
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)
|
||||||
|
|
||||||
|
|
||||||
class Page(models.Model):
|
class Page(models.Model):
|
||||||
""" Base class for publishable content """
|
""" Base class for publishable content """
|
||||||
|
@ -38,13 +61,18 @@ class Page(models.Model):
|
||||||
default=STATUS.draft,
|
default=STATUS.draft,
|
||||||
choices=[(int(y), _(x)) for x, y in STATUS.__members__.items()],
|
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(
|
cover = FilerImageField(
|
||||||
on_delete=models.SET_NULL, null=True, blank=True,
|
on_delete=models.SET_NULL,
|
||||||
verbose_name=_('Cover'),
|
verbose_name=_('Cover'), null=True, blank=True,
|
||||||
)
|
)
|
||||||
content = RichTextField(
|
content = RichTextField(
|
||||||
_('content'), blank=True, null=True,
|
_('content'), blank=True, null=True,
|
||||||
)
|
)
|
||||||
|
date = models.DateTimeField(default=tz.now)
|
||||||
featured = models.BooleanField(
|
featured = models.BooleanField(
|
||||||
_('featured'), default=False,
|
_('featured'), default=False,
|
||||||
)
|
)
|
||||||
|
@ -86,6 +114,23 @@ class Page(models.Model):
|
||||||
def is_trash(self):
|
def is_trash(self):
|
||||||
return self.status == self.STATUS.trash
|
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):
|
class NavItem(models.Model):
|
||||||
""" Navigation menu items """
|
""" Navigation menu items """
|
||||||
|
|
|
@ -470,7 +470,8 @@ class Schedule(BaseRerun):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if initial is None:
|
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
|
episodes[date] = episode
|
||||||
else:
|
else:
|
||||||
episode = episodes[initial]
|
episode = episodes[initial]
|
||||||
|
@ -489,10 +490,6 @@ class Schedule(BaseRerun):
|
||||||
if self.initial is not None and self.date > self.date:
|
if self.initial is not None and self.date > self.date:
|
||||||
raise ValueError('initial must be later')
|
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):
|
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
|
from django.conf import settings
|
||||||
|
|
||||||
def ensure (key, default):
|
|
||||||
|
def ensure(key, default):
|
||||||
globals()[key] = getattr(settings, key, default)
|
globals()[key] = getattr(settings, key, default)
|
||||||
|
|
||||||
|
|
||||||
########################################################################
|
########################################################################
|
||||||
# Global & misc
|
# Global & misc
|
||||||
########################################################################
|
########################################################################
|
||||||
|
@ -48,7 +50,7 @@ ensure('AIRCOX_EPISODE_TITLE_DATE_FORMAT', '%-d %B %Y')
|
||||||
# Directory where to save logs' archives
|
# Directory where to save logs' archives
|
||||||
ensure('AIRCOX_LOGS_ARCHIVES_DIR',
|
ensure('AIRCOX_LOGS_ARCHIVES_DIR',
|
||||||
os.path.join(AIRCOX_DATA_DIR, 'archives')
|
os.path.join(AIRCOX_DATA_DIR, 'archives')
|
||||||
)
|
)
|
||||||
# In days, minimal age of a log before it is archived
|
# In days, minimal age of a log before it is archived
|
||||||
ensure('AIRCOX_LOGS_ARCHIVES_MIN_AGE', 60)
|
ensure('AIRCOX_LOGS_ARCHIVES_MIN_AGE', 60)
|
||||||
|
|
||||||
|
@ -70,21 +72,21 @@ ensure('AIRCOX_SOUND_AUTO_CHMOD', True)
|
||||||
# and stat.*
|
# and stat.*
|
||||||
ensure(
|
ensure(
|
||||||
'AIRCOX_SOUND_CHMOD_FLAGS',
|
'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
|
# Quality attributes passed to sound_quality_check from sounds_monitor
|
||||||
ensure('AIRCOX_SOUND_QUALITY', {
|
ensure('AIRCOX_SOUND_QUALITY', {
|
||||||
'attribute': 'RMS lev dB',
|
'attribute': 'RMS lev dB',
|
||||||
'range': (-18.0, -8.0),
|
'range': (-18.0, -8.0),
|
||||||
'sample_length': 120,
|
'sample_length': 120,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Extension of sound files
|
# Extension of sound files
|
||||||
ensure(
|
ensure(
|
||||||
'AIRCOX_SOUND_FILE_EXT',
|
'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', ';')
|
ensure('AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER', ';')
|
||||||
# Text delimiter of csv text files
|
# Text delimiter of csv text files
|
||||||
ensure('AIRCOX_IMPORT_PLAYLIST_CSV_TEXT_QUOTE', '"')
|
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;
|
float: right;
|
||||||
max-width: 45%; }
|
max-width: 45%; }
|
||||||
|
|
||||||
.page > .header {
|
.page .header {
|
||||||
margin-bottom: 1.5em; }
|
margin-bottom: 1.5em; }
|
||||||
|
|
||||||
.page .headline {
|
.page .headline {
|
||||||
|
@ -7179,6 +7179,11 @@ label.panel-block {
|
||||||
.page p {
|
.page p {
|
||||||
padding: 0.4em 0em; }
|
padding: 0.4em 0em; }
|
||||||
|
|
||||||
|
section > .toolbar {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 1em;
|
||||||
|
margin-bottom: 1.5em; }
|
||||||
|
|
||||||
.cover {
|
.cover {
|
||||||
margin: 1em 0em;
|
margin: 1em 0em;
|
||||||
border: 0.2em black solid; }
|
border: 0.2em black solid; }
|
||||||
|
|
|
@ -5,10 +5,11 @@ Context:
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8" />
|
||||||
<meta name="application-name" content="aircox">
|
<meta name="application-name" content="aircox" />
|
||||||
<meta name="description" content="{{ site.description }}">
|
<meta name="description" content="{{ site.description }}" />
|
||||||
<meta name="keywords" content="{{ site.tags }}">
|
<meta name="keywords" content="{{ site.tags }}" />
|
||||||
|
<meta name="generator" content="Aircox" />
|
||||||
<link rel="icon" href="{% thumbnail site.favicon 32x32 crop %}" />
|
<link rel="icon" href="{% thumbnail site.favicon 32x32 crop %}" />
|
||||||
|
|
||||||
{% block assets %}
|
{% block assets %}
|
||||||
|
@ -18,7 +19,7 @@ Context:
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
<title>
|
<title>
|
||||||
{% block head_title %}{{ site.title }}{% endblock %}
|
{% block head_title %}{{ station.name }}{% endblock %}
|
||||||
</title>
|
</title>
|
||||||
|
|
||||||
{% block head_extra %}{% endblock %}
|
{% block head_extra %}{% endblock %}
|
||||||
|
@ -52,12 +53,14 @@ Context:
|
||||||
{% block header %}
|
{% block header %}
|
||||||
<h1 class="title is-1">{% block title %}{% endblock %}</h1>
|
<h1 class="title is-1">{% block title %}{% endblock %}</h1>
|
||||||
|
|
||||||
{% if parent %}
|
<h4 class="subtitle is-size-3 columns">
|
||||||
<h4 class="subtitle is-size-3">
|
{% block subtitle %}
|
||||||
<a href="{{ parent.get_absolute_url }}">
|
{% if parent %}
|
||||||
|
<a href="{{ parent.get_absolute_url }}" class="column">
|
||||||
❬ {{ parent.title }}</a></li>
|
❬ {{ parent.title }}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
</h4>
|
</h4>
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</header>
|
</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 %}
|
{% for day, diffusions in by_date.items %}
|
||||||
<noscript><h4 class="subtitle is-4">{{ day|date:"l d F Y" }}</h4></noscript>
|
<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 }}'">
|
<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="columns">
|
||||||
<div class="column is-one-fifth has-text-right">
|
<div class="column is-one-fifth has-text-right">
|
||||||
<time datetime="{{ object.start|date:"c" }}">
|
<time datetime="{{ diffusion.start|date:"c" }}">
|
||||||
{{ object.start|date:"H:i" }} - {{ object.end|date:"H:i" }}
|
{{ diffusion.start|date:"H:i" }} - {{ diffusion.end|date:"H:i" }}
|
||||||
</time>
|
</time>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
{% include "aircox/diffusion_item.html" %}
|
{% with diffusion.episode as object %}
|
||||||
|
{% include "aircox/episode_item.html" %}
|
||||||
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -1,6 +1,33 @@
|
||||||
{% extends "aircox/program_base.html" %}
|
{% extends "aircox/program_base.html" %}
|
||||||
{% load i18n %}
|
{% 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 main %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,11 @@
|
||||||
{{ object.start|date:"H:i" }} - {{ object.end|date:"H:i" }}
|
{{ object.start|date:"H:i" }} - {{ object.end|date:"H:i" }}
|
||||||
</time>
|
</time>
|
||||||
</td>
|
</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 %}
|
{% else %}
|
||||||
<td>
|
<td>
|
||||||
<time datetime="{{ object.date }}" title="{{ object.date }}">
|
<time datetime="{{ object.date }}" title="{{ object.date }}">
|
||||||
|
|
|
@ -7,10 +7,17 @@ Context:
|
||||||
- page: page
|
- page: page
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
|
{% block subtitle %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% if page.category %}
|
||||||
|
<span class="column has-text-right">{{ page.category.title }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block head_title %}
|
{% block head_title %}
|
||||||
{% block title %}{{ title }}{% endblock %}
|
{% block title %}{{ title }}{% endblock %}
|
||||||
{% if title %} — {% endif %}
|
{% if title %} ‐ {% endif %}
|
||||||
{{ site.title }}
|
{{ station.name }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<div class="podcast">
|
<div class="podcast">
|
||||||
{% if object.embed %}
|
{% if object.embed %}
|
||||||
{{ object.embed }}
|
{{ object.embed|safe }}
|
||||||
{% else %}
|
{% else %}
|
||||||
<audio src="{{ object.url }}" controls>
|
<audio src="{{ object.url }}" controls>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -4,22 +4,22 @@
|
||||||
{% block side_nav %}
|
{% block side_nav %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
|
||||||
{% if diffusions %}
|
{% if episodes %}
|
||||||
<section>
|
<section>
|
||||||
<h4 class="subtitle is-size-4">{% trans "Last shows" %}</h4>
|
<h4 class="subtitle is-size-4">{% trans "Last shows" %}</h4>
|
||||||
|
|
||||||
{% for object in diffusions %}
|
{% for object in episodes %}
|
||||||
{% include "aircox/diffusion_item.html" %}
|
{% include "aircox/episode_item.html" %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
<nav class="pagination is-centered">
|
<nav class="pagination is-centered">
|
||||||
<ul class="pagination-list">
|
<ul class="pagination-list">
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url "diffusion-list" program_slug=page.slug %}"
|
<a href="{% url "diffusion-list" program_slug=program.slug %}"
|
||||||
class="pagination-link"
|
class="pagination-link"
|
||||||
aria-label="{% trans "Show all diffusions" %}">
|
aria-label="{% trans "Show all diffusions" %}">
|
||||||
{% trans "All diffusions" %}
|
{% trans "All shows" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -1,25 +1,24 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<section class="is-size-5">
|
<section class="is-size-5 has-text-weight-bold">
|
||||||
{% for schedule in program.schedule_set.all %}
|
{% for schedule in program.schedule_set.all %}
|
||||||
<p>
|
{{ schedule.get_frequency_verbose }}
|
||||||
{{ schedule.get_frequency_verbose }}
|
{% with schedule.start|date:"H:i" as start %}
|
||||||
{% with schedule.start|date:"H:i" as start %}
|
{% with schedule.end|date:"H:i" as end %}
|
||||||
{% with schedule.end|date:"H:i" as end %}
|
<time datetime="{{ start }}">{{ start }}</time>
|
||||||
<time datetime="{{ start }}">{{ start }}</time>
|
—
|
||||||
—
|
<time datetime="{{ end }}">{{ end }}</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 %}
|
||||||
{% endwith %}
|
{% endif %}
|
||||||
<small>
|
</small>
|
||||||
{% if schedule.initial %}
|
<br>
|
||||||
{% with schedule.initial.date as date %}
|
|
||||||
<span title="{% blocktrans %}Rerun of {{ date }}{% endblocktrans %}">
|
|
||||||
({% trans "rerun" %})
|
|
||||||
</span>
|
|
||||||
{% endwith %}
|
|
||||||
{% endif %}
|
|
||||||
</small>
|
|
||||||
</p>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
|
@ -8,17 +8,33 @@ random.seed()
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(name='unique_id')
|
@register.filter(name='verbose_name')
|
||||||
def do_unique_id(prefix=''):
|
def do_verbose_name(obj, plural=False):
|
||||||
value = str(random.random()).replace('.', '')
|
""" Return model's verbose name (singular or plural) """
|
||||||
return prefix + '_' + value if prefix else value
|
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')
|
@register.filter(name='is_diffusion')
|
||||||
def do_is_diffusion(obj):
|
def do_is_diffusion(obj):
|
||||||
return isinstance(obj, Diffusion)
|
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)
|
@register.simple_tag(name='nav_items', takes_context=True)
|
||||||
def do_nav_items(context, menu, **kwargs):
|
def do_nav_items(context, menu, **kwargs):
|
||||||
station, request = context['station'], context['request']
|
station, request = context['station'], context['request']
|
||||||
|
|
|
@ -11,20 +11,22 @@ from aircox.models import *
|
||||||
logger = logging.getLogger('aircox.test')
|
logger = logging.getLogger('aircox.test')
|
||||||
logger.setLevel('INFO')
|
logger.setLevel('INFO')
|
||||||
|
|
||||||
|
|
||||||
class ScheduleCheck (TestCase):
|
class ScheduleCheck (TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.schedules = [
|
self.schedules = [
|
||||||
Schedule(
|
Schedule(
|
||||||
date = tz.now(),
|
date=tz.now(),
|
||||||
duration = datetime.time(1,30),
|
duration=datetime.time(1, 30),
|
||||||
frequency = frequency,
|
frequency=frequency,
|
||||||
)
|
)
|
||||||
for frequency in Schedule.Frequency.__members__.values()
|
for frequency in Schedule.Frequency.__members__.values()
|
||||||
]
|
]
|
||||||
|
|
||||||
def test_frequencies(self):
|
def test_frequencies(self):
|
||||||
for schedule in self.schedules:
|
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
|
date = schedule.date
|
||||||
count = 24
|
count = 24
|
||||||
while count:
|
while count:
|
||||||
|
@ -40,7 +42,7 @@ class ScheduleCheck (TestCase):
|
||||||
self.check_last(schedule, date, dates)
|
self.check_last(schedule, date, dates)
|
||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
date += relativedelta(months = 1)
|
date += relativedelta(months=1)
|
||||||
|
|
||||||
def check_one_on_two(self, schedule, date, dates):
|
def check_one_on_two(self, schedule, date, dates):
|
||||||
for date in dates:
|
for date in dates:
|
||||||
|
@ -53,14 +55,11 @@ class ScheduleCheck (TestCase):
|
||||||
|
|
||||||
# end of month before the wanted weekday: move one week back
|
# end of month before the wanted weekday: move one week back
|
||||||
if date.weekday() < schedule.date.weekday():
|
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=date.weekday())
|
||||||
date += datetime.timedelta(days = schedule.date.weekday())
|
date += datetime.timedelta(days=schedule.date.weekday())
|
||||||
self.assertEqual(date, dates[0].date())
|
self.assertEqual(date, dates[0].date())
|
||||||
|
|
||||||
def check_n_of_week(self, schedule, date, dates):
|
def check_n_of_week(self, schedule, date, dates):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from django.urls import path, register_converter
|
from django.urls import path, register_converter
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from . import views
|
from . import views, models
|
||||||
from .converters import PagePathConverter, DateConverter, WeekConverter
|
from .converters import PagePathConverter, DateConverter, WeekConverter
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,21 +10,32 @@ register_converter(DateConverter, 'date')
|
||||||
register_converter(WeekConverter, 'week')
|
register_converter(WeekConverter, 'week')
|
||||||
|
|
||||||
|
|
||||||
#urls = [
|
# urls = [
|
||||||
# path('on_air', views.on_air, name='aircox.on_air'),
|
# path('on_air', views.on_air, name='aircox.on_air'),
|
||||||
# path('monitor', views.Monitor.as_view(), name='aircox.monitor'),
|
# path('monitor', views.Monitor.as_view(), name='aircox.monitor'),
|
||||||
# path('stats', views.StatisticsView.as_view(), name='aircox.stats'),
|
# path('stats', views.StatisticsView.as_view(), name='aircox.stats'),
|
||||||
#]
|
# ]
|
||||||
|
|
||||||
|
|
||||||
urls = [
|
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>/'),
|
path(_('programs/<slug:slug>/'),
|
||||||
views.ProgramDetailView.as_view(), name='program-detail'),
|
views.ProgramDetailView.as_view(), name='program-detail'),
|
||||||
path(_('programs/<slug:program_slug>/episodes/'),
|
path(_('programs/<slug:program_slug>/episodes/'),
|
||||||
views.DiffusionListView.as_view(), name='diffusion-list'),
|
views.EpisodeListView.as_view(), name='diffusion-list'),
|
||||||
|
|
||||||
path(_('episodes/'),
|
path(_('episodes/'),
|
||||||
views.DiffusionListView.as_view(), name='diffusion-list'),
|
views.EpisodeListView.as_view(), name='diffusion-list'),
|
||||||
path(_('episodes/week/'),
|
path(_('episodes/week/'),
|
||||||
views.TimetableView.as_view(), name='timetable'),
|
views.TimetableView.as_view(), name='timetable'),
|
||||||
path(_('episodes/week/<week:date>/'),
|
path(_('episodes/week/<week:date>/'),
|
||||||
|
@ -36,5 +47,3 @@ urls = [
|
||||||
path(_('logs/<date:date>/'), views.LogListView.as_view(), name='logs'),
|
path(_('logs/<date:date>/'), views.LogListView.as_view(), name='logs'),
|
||||||
# path('<page_path:path>', views.route_page, name='page'),
|
# path('<page_path:path>', views.route_page, name='page'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ __all__ = ['Redirect', 'redirect', 'date_range', 'cast_date',
|
||||||
|
|
||||||
class Redirect(Exception):
|
class Redirect(Exception):
|
||||||
""" Redirect exception -- see `redirect()`. """
|
""" Redirect exception -- see `redirect()`. """
|
||||||
|
|
||||||
def __init__(self, url):
|
def __init__(self, url):
|
||||||
self.url = url
|
self.url = url
|
||||||
|
|
||||||
|
@ -82,4 +83,3 @@ def seconds_to_time(seconds):
|
||||||
minutes, seconds = divmod(seconds, 60)
|
minutes, seconds = divmod(seconds, 60)
|
||||||
hours, minutes = divmod(minutes, 60)
|
hours, minutes = divmod(minutes, 60)
|
||||||
return datetime.time(hour=hours, minute=minutes, second=seconds)
|
return datetime.time(hour=hours, minute=minutes, second=seconds)
|
||||||
|
|
||||||
|
|
|
@ -2,17 +2,15 @@ import os
|
||||||
import json
|
import json
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from django.db.models import Count
|
|
||||||
from django.views.generic.base import View, TemplateResponseMixin
|
from django.views.generic.base import View, TemplateResponseMixin
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.http import HttpResponse, Http404
|
from django.http import HttpResponse, Http404
|
||||||
from django.shortcuts import render
|
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.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.models as models
|
||||||
import aircox.settings as settings
|
|
||||||
|
|
||||||
|
|
||||||
# FIXME usefull?
|
# FIXME usefull?
|
||||||
|
@ -29,6 +27,7 @@ class Stations:
|
||||||
for station in self.stations:
|
for station in self.stations:
|
||||||
station.streamer.fetch()
|
station.streamer.fetch()
|
||||||
|
|
||||||
|
|
||||||
stations = Stations()
|
stations = Stations()
|
||||||
|
|
||||||
|
|
||||||
|
@ -102,7 +101,7 @@ class Monitor(View, TemplateResponseMixin, LoginRequiredMixin):
|
||||||
return HttpResponse('')
|
return HttpResponse('')
|
||||||
|
|
||||||
POST = request.POST
|
POST = request.POST
|
||||||
controller = POST.get('controller')
|
POST.get('controller')
|
||||||
action = POST.get('action')
|
action = POST.get('action')
|
||||||
|
|
||||||
station = stations.stations.filter(name=POST.get('station')) \
|
station = stations.stations.filter(name=POST.get('station')) \
|
||||||
|
@ -256,5 +255,3 @@ class StatisticsView(View, TemplateResponseMixin, LoginRequiredMixin):
|
||||||
self.request = request
|
self.request = request
|
||||||
context = self.get_context_data(**kwargs)
|
context = self.get_context_data(**kwargs)
|
||||||
return render(request, self.template_name, context)
|
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%;
|
max-width: 45%;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .header {
|
.header {
|
||||||
margin-bottom: 1.5em;
|
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 {
|
.cover {
|
||||||
margin: 1em 0em;
|
margin: 1em 0em;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user