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