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