diff --git a/aircox/__init__.py b/aircox/__init__.py index 877e873..0b70342 100755 --- a/aircox/__init__.py +++ b/aircox/__init__.py @@ -1,3 +1,2 @@ default_app_config = 'aircox.apps.AircoxConfig' - diff --git a/aircox/admin/__init__.py b/aircox/admin/__init__.py index 52f64e1..21436bb 100644 --- a/aircox/admin/__init__.py +++ b/aircox/admin/__init__.py @@ -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 diff --git a/aircox/admin/__pycache__/__init__.cpython-37.pyc b/aircox/admin/__pycache__/__init__.cpython-37.pyc index c11ecc4..8a13be3 100644 Binary files a/aircox/admin/__pycache__/__init__.cpython-37.pyc and b/aircox/admin/__pycache__/__init__.cpython-37.pyc differ diff --git a/aircox/admin/__pycache__/episode.cpython-37.pyc b/aircox/admin/__pycache__/episode.cpython-37.pyc index 8ef6f1c..efe1b5b 100644 Binary files a/aircox/admin/__pycache__/episode.cpython-37.pyc and b/aircox/admin/__pycache__/episode.cpython-37.pyc differ diff --git a/aircox/admin/__pycache__/page.cpython-37.pyc b/aircox/admin/__pycache__/page.cpython-37.pyc index db50754..028ce77 100644 Binary files a/aircox/admin/__pycache__/page.cpython-37.pyc and b/aircox/admin/__pycache__/page.cpython-37.pyc differ diff --git a/aircox/admin/__pycache__/sound.cpython-37.pyc b/aircox/admin/__pycache__/sound.cpython-37.pyc index d4464c9..4480626 100644 Binary files a/aircox/admin/__pycache__/sound.cpython-37.pyc and b/aircox/admin/__pycache__/sound.cpython-37.pyc differ diff --git a/aircox/admin/article.py b/aircox/admin/article.py new file mode 100644 index 0000000..06e2bbc --- /dev/null +++ b/aircox/admin/article.py @@ -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') + diff --git a/aircox/admin/episode.py b/aircox/admin/episode.py index b3f7799..ce307ae 100644 --- a/aircox/admin/episode.py +++ b/aircox/admin/episode.py @@ -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: diff --git a/aircox/admin/page.py b/aircox/admin/page.py index 1810b97..3370e3a 100644 --- a/aircox/admin/page.py +++ b/aircox/admin/page.py @@ -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 + + diff --git a/aircox/admin/playlist.py b/aircox/admin/playlist.py deleted file mode 100644 index 1879c1e..0000000 --- a/aircox/admin/playlist.py +++ /dev/null @@ -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 - - diff --git a/aircox/admin/sound.py b/aircox/admin/sound.py index a166977..2499fd6 100644 --- a/aircox/admin/sound.py +++ b/aircox/admin/sound.py @@ -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'] + diff --git a/aircox/apps.py b/aircox/apps.py index f95d3cb..81a337b 100755 --- a/aircox/apps.py +++ b/aircox/apps.py @@ -6,5 +6,4 @@ class AircoxConfig(AppConfig): verbose_name = 'Aircox' def ready(self): - import aircox.signals - + pass diff --git a/aircox/connector.py b/aircox/connector.py index 3a0499b..e2cba5a 100755 --- a/aircox/connector.py +++ b/aircox/connector.py @@ -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 - diff --git a/aircox/controllers.py b/aircox/controllers.py index 931f8d2..8977d91 100755 --- a/aircox/controllers.py +++ b/aircox/controllers.py @@ -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 - - diff --git a/aircox/converters.py b/aircox/converters.py index d6ca605..b694fd3 100644 --- a/aircox/converters.py +++ b/aircox/converters.py @@ -45,4 +45,3 @@ class DateConverter: def to_url(self, value): return '{:04d}/{:02d}/{:02d}'.format(value.year, value.month, value.day) - diff --git a/aircox/middleware.py b/aircox/middleware.py index 67b0d80..b64f004 100644 --- a/aircox/middleware.py +++ b/aircox/middleware.py @@ -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 diff --git a/aircox/models/__init__.py b/aircox/models/__init__.py index e586ca0..18bef4e 100644 --- a/aircox/models/__init__.py +++ b/aircox/models/__init__.py @@ -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 + diff --git a/aircox/models/__pycache__/__init__.cpython-37.pyc b/aircox/models/__pycache__/__init__.cpython-37.pyc index 932d6d5..f5130d9 100644 Binary files a/aircox/models/__pycache__/__init__.cpython-37.pyc and b/aircox/models/__pycache__/__init__.cpython-37.pyc differ diff --git a/aircox/models/__pycache__/episode.cpython-37.pyc b/aircox/models/__pycache__/episode.cpython-37.pyc index ea46f1c..1adab9a 100644 Binary files a/aircox/models/__pycache__/episode.cpython-37.pyc and b/aircox/models/__pycache__/episode.cpython-37.pyc differ diff --git a/aircox/models/__pycache__/page.cpython-37.pyc b/aircox/models/__pycache__/page.cpython-37.pyc index b11411d..5bc0957 100644 Binary files a/aircox/models/__pycache__/page.cpython-37.pyc and b/aircox/models/__pycache__/page.cpython-37.pyc differ diff --git a/aircox/models/__pycache__/program.cpython-37.pyc b/aircox/models/__pycache__/program.cpython-37.pyc index b22dc2e..90e1f0b 100644 Binary files a/aircox/models/__pycache__/program.cpython-37.pyc and b/aircox/models/__pycache__/program.cpython-37.pyc differ diff --git a/aircox/models/article.py b/aircox/models/article.py new file mode 100644 index 0000000..8986eb6 --- /dev/null +++ b/aircox/models/article.py @@ -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') + + + diff --git a/aircox/models/episode.py b/aircox/models/episode.py index 37af2b1..d145db4 100644 --- a/aircox/models/episode.py +++ b/aircox/models/episode.py @@ -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): diff --git a/aircox/models/page.py b/aircox/models/page.py index 959ef7d..8fc61e7 100644 --- a/aircox/models/page.py +++ b/aircox/models/page.py @@ -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'(

)?' + r'(?P[^\n]{1,140}(\n|[^\.]*?\.))' + r'(

)?') + + +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 """ diff --git a/aircox/models/program.py b/aircox/models/program.py index 9852f1d..dba587d 100644 --- a/aircox/models/program.py +++ b/aircox/models/program.py @@ -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): """ diff --git a/aircox/models/signals.py b/aircox/models/signals.py new file mode 100755 index 0000000..7b2ad76 --- /dev/null +++ b/aircox/models/signals.py @@ -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() + + diff --git a/aircox/settings.py b/aircox/settings.py index 682d776..43ffbca 100755 --- a/aircox/settings.py +++ b/aircox/settings.py @@ -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', '"') - - - diff --git a/aircox/signals.py b/aircox/signals.py deleted file mode 100755 index 8844968..0000000 --- a/aircox/signals.py +++ /dev/null @@ -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() - - diff --git a/aircox/static/aircox/main.css b/aircox/static/aircox/main.css index b988d70..35c8116 100644 --- a/aircox/static/aircox/main.css +++ b/aircox/static/aircox/main.css @@ -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; } diff --git a/aircox/templates/aircox/base.html b/aircox/templates/aircox/base.html index 2349978..4c5d6b3 100644 --- a/aircox/templates/aircox/base.html +++ b/aircox/templates/aircox/base.html @@ -5,10 +5,11 @@ Context: {% endcomment %} - - - - + + + + + {% block assets %} @@ -18,7 +19,7 @@ Context: {% endblock %} - {% block head_title %}{{ site.title }}{% endblock %} + {% block head_title %}{{ station.name }}{% endblock %} {% block head_extra %}{% endblock %} @@ -52,12 +53,14 @@ Context: {% block header %}

{% block title %}{% endblock %}

- {% if parent %} -

- +

+ {% block subtitle %} + {% if parent %} + ❬ {{ parent.title }} + {% endif %} + {% endblock %}

- {% endif %} {% endblock %} diff --git a/aircox/templates/aircox/diffusion_item.html b/aircox/templates/aircox/diffusion_item.html deleted file mode 100644 index b907276..0000000 --- a/aircox/templates/aircox/diffusion_item.html +++ /dev/null @@ -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 %} -
-
- -
-
-
- {% if episode.is_published %} - {{ episode.title }} - {% endif %} -
- -
- {% if not page or program != page %} - {% if program.is_published %} - - {{ program.title }} - {% else %}{{ program.title }} - {% endif %} - {% if not hide_schedule %} — {% endif %} - {% endif %} - - {% if not hide_schedule %} - - {% endif %} - - {% if object.initial %} - {% with object.initial.date as date %} - - {% trans "rerun" %} - - {% endwith %} - {% endif %} - -
- {% if not hide_headline %} -
- {{ episode.headline }} -
- {% endif %} -
-
-{% endwith %} -{% endwith %} - diff --git a/aircox/templates/aircox/diffusion_list.html b/aircox/templates/aircox/diffusion_list.html deleted file mode 100644 index 57850b1..0000000 --- a/aircox/templates/aircox/diffusion_list.html +++ /dev/null @@ -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 %} -
- {% for object in object_list %} - {% include "aircox/diffusion_item.html" %} - {% endfor %} -
- - -{% if is_paginated %} - -{% endif %} - -{% endblock %} - diff --git a/aircox/templates/aircox/diffusion_timetable.html b/aircox/templates/aircox/diffusion_timetable.html index 24fc2cf..4a30fd5 100644 --- a/aircox/templates/aircox/diffusion_timetable.html +++ b/aircox/templates/aircox/diffusion_timetable.html @@ -34,15 +34,17 @@ {% for day, diffusions in by_date.items %}
- {% for object in diffusions %} + {% for diffusion in diffusions %}
-
- {% include "aircox/diffusion_item.html" %} + {% with diffusion.episode as object %} + {% include "aircox/episode_item.html" %} + {% endwith %}
{% endfor %} diff --git a/aircox/templates/aircox/episode_detail.html b/aircox/templates/aircox/episode_detail.html index c0ee957..3f1fda9 100644 --- a/aircox/templates/aircox/episode_detail.html +++ b/aircox/templates/aircox/episode_detail.html @@ -1,6 +1,33 @@ {% extends "aircox/program_base.html" %} {% load i18n %} +{% block header %} +{{ block.super }} + +
+ {% for diffusion in object.diffusion_set.all %} + {% with diffusion.start as start %} + {% with diffusion.end as end %} + + — + + {% endwith %} + {% endwith %} + + + {% if diffusion.initial %} + {% with diffusion.initial.date as date %} + + ({% trans "rerun" %}) + + {% endwith %} + {% endif %} + +
+ {% endfor %} +
+{% endblock %} + {% block main %} {{ block.super }} diff --git a/aircox/templates/aircox/log_list.html b/aircox/templates/aircox/log_list.html index e73198f..4fa4830 100644 --- a/aircox/templates/aircox/log_list.html +++ b/aircox/templates/aircox/log_list.html @@ -39,7 +39,11 @@ {{ object.start|date:"H:i" }} - {{ object.end|date:"H:i" }} - {% include "aircox/diffusion_item.html" %} + {% with object as diffusion %} + {% with diffusion.episode as object %} + {% include "aircox/episode_item.html" %} + {% endwith %} + {% endwith %} {% else %}