work on website; fix stuffs on aircox too
This commit is contained in:
		@ -84,8 +84,8 @@ class StationAdmin(admin.ModelAdmin):
 | 
			
		||||
 | 
			
		||||
@admin.register(Log)
 | 
			
		||||
class LogAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ['id', 'date', 'station', 'source', 'type', 'comment', 'diffusion', 'sound', 'track']
 | 
			
		||||
    list_filter = ['date', 'source', 'diffusion', 'sound', 'track']
 | 
			
		||||
    list_display = ['id', 'date', 'station', 'source', 'type', 'diffusion', 'sound', 'track']
 | 
			
		||||
    list_filter = ['date', 'source', 'station']
 | 
			
		||||
 | 
			
		||||
admin.site.register(Port)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -34,7 +34,7 @@ class DiffusionAdmin(admin.ModelAdmin):
 | 
			
		||||
    conflicts_count.short_description = _('Conflicts')
 | 
			
		||||
 | 
			
		||||
    def start_date(self, obj):
 | 
			
		||||
        return obj.local_date.strftime('%Y/%m/%d %H:%M')
 | 
			
		||||
        return obj.local_start.strftime('%Y/%m/%d %H:%M')
 | 
			
		||||
    start_date.short_description = _('start')
 | 
			
		||||
 | 
			
		||||
    def end_date(self, obj):
 | 
			
		||||
 | 
			
		||||
@ -18,11 +18,10 @@ class TracksInline(SortableInlineAdminMixin, admin.TabularInline):
 | 
			
		||||
 | 
			
		||||
@admin.register(Track)
 | 
			
		||||
class TrackAdmin(admin.ModelAdmin):
 | 
			
		||||
    # TODO: url to filter by tag
 | 
			
		||||
    def tag_list(self, obj):
 | 
			
		||||
        return u", ".join(o.name for o in obj.tags.all())
 | 
			
		||||
 | 
			
		||||
    list_display = ['pk', 'artist', 'title', 'tag_list', 'diffusion', 'sound']
 | 
			
		||||
    list_display = ['pk', 'artist', 'title', 'tag_list', 'diffusion', 'sound', 'timestamp']
 | 
			
		||||
    list_editable = ['artist', 'title']
 | 
			
		||||
    list_filter = ['sound', 'diffusion', 'artist', 'title', 'tags']
 | 
			
		||||
    fieldsets = [
 | 
			
		||||
 | 
			
		||||
@ -16,14 +16,14 @@ logger = logging.getLogger('aircox.tools')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command (BaseCommand):
 | 
			
		||||
    help= __doc__
 | 
			
		||||
    help = __doc__
 | 
			
		||||
 | 
			
		||||
    def add_arguments (self, parser):
 | 
			
		||||
        parser.formatter_class=RawTextHelpFormatter
 | 
			
		||||
    def add_arguments(self, parser):
 | 
			
		||||
        parser.formatter_class = RawTextHelpFormatter
 | 
			
		||||
        group = parser.add_argument_group('actions')
 | 
			
		||||
        group.add_argument(
 | 
			
		||||
            '-a', '--age', type=int,
 | 
			
		||||
            default = settings.AIRCOX_LOGS_ARCHIVES_MIN_AGE,
 | 
			
		||||
            default=settings.AIRCOX_LOGS_ARCHIVES_MIN_AGE,
 | 
			
		||||
            help='minimal age in days of logs to archive. Default is '
 | 
			
		||||
                 'settings.AIRCOX_LOGS_ARCHIVES_MIN_AGE'
 | 
			
		||||
        )
 | 
			
		||||
@ -36,22 +36,21 @@ class Command (BaseCommand):
 | 
			
		||||
            help='keep logs in database instead of deleting them'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def handle (self, *args, age, force, keep, **options):
 | 
			
		||||
        date = tz.now() - tz.timedelta(days = age)
 | 
			
		||||
    def handle(self, *args, age, force, keep, **options):
 | 
			
		||||
        date = tz.now() - tz.timedelta(days=age)
 | 
			
		||||
 | 
			
		||||
        while True:
 | 
			
		||||
            date = date.replace(
 | 
			
		||||
                hour = 0, minute = 0, second = 0, microsecond = 0
 | 
			
		||||
                hour=0, minute=0, second=0, microsecond=0
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            logger.info('archive log at date %s', date)
 | 
			
		||||
            for station in Station.objects.all():
 | 
			
		||||
                Log.objects.make_archive(
 | 
			
		||||
                    station, date, force = force, keep = keep
 | 
			
		||||
                    station, date, force=force, keep=keep
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            qs = Log.objects.filter(date__lt = date)
 | 
			
		||||
            qs = Log.objects.filter(date__lt=date)
 | 
			
		||||
            if not qs.exists():
 | 
			
		||||
                break
 | 
			
		||||
            date = qs.order_by('-date').first().date
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -15,6 +15,7 @@ planified before the (given) month.
 | 
			
		||||
- "check" will remove all diffusions that are unconfirmed and have been planified
 | 
			
		||||
from the (given) month and later.
 | 
			
		||||
"""
 | 
			
		||||
import time
 | 
			
		||||
import logging
 | 
			
		||||
from argparse import RawTextHelpFormatter
 | 
			
		||||
 | 
			
		||||
@ -25,53 +26,52 @@ from aircox.models import *
 | 
			
		||||
 | 
			
		||||
logger = logging.getLogger('aircox.tools')
 | 
			
		||||
 | 
			
		||||
import time
 | 
			
		||||
 | 
			
		||||
class Actions:
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def update (cl, date, mode):
 | 
			
		||||
    def update(cl, date, mode):
 | 
			
		||||
        manual = (mode == 'manual')
 | 
			
		||||
 | 
			
		||||
        count = [0, 0]
 | 
			
		||||
        for schedule in Schedule.objects.filter(program__active = True) \
 | 
			
		||||
        for schedule in Schedule.objects.filter(program__active=True) \
 | 
			
		||||
                .order_by('initial'):
 | 
			
		||||
            # in order to allow rerun links between diffusions, we save items
 | 
			
		||||
            # by schedule;
 | 
			
		||||
            items = schedule.diffusions_of_month(date, exclude_saved = True)
 | 
			
		||||
            items = schedule.diffusions_of_month(date, exclude_saved=True)
 | 
			
		||||
            count[0] += len(items)
 | 
			
		||||
 | 
			
		||||
            # we can't bulk create because we need signal processing
 | 
			
		||||
            for item in items:
 | 
			
		||||
                conflicts = item.get_conflicts()
 | 
			
		||||
                item.type = Diffusion.Type.unconfirmed \
 | 
			
		||||
                                if manual or conflicts.count() else \
 | 
			
		||||
                            Diffusion.Type.normal
 | 
			
		||||
                item.save(no_check = True)
 | 
			
		||||
                    if manual or conflicts.count() else \
 | 
			
		||||
                    Diffusion.Type.normal
 | 
			
		||||
                item.save(no_check=True)
 | 
			
		||||
                if conflicts.count():
 | 
			
		||||
                    item.conflicts.set(conflicts.all())
 | 
			
		||||
 | 
			
		||||
            logger.info('[update] schedule %s: %d new diffusions',
 | 
			
		||||
                    str(schedule), len(items),
 | 
			
		||||
                 )
 | 
			
		||||
                        str(schedule), len(items),
 | 
			
		||||
                        )
 | 
			
		||||
 | 
			
		||||
        logger.info('[update] %d diffusions have been created, %s', count[0],
 | 
			
		||||
              'do not forget manual approval' if manual else
 | 
			
		||||
                '{} conflicts found'.format(count[1]))
 | 
			
		||||
                    'do not forget manual approval' if manual else
 | 
			
		||||
                    '{} conflicts found'.format(count[1]))
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def clean (date):
 | 
			
		||||
        qs = Diffusion.objects.filter(type = Diffusion.Type.unconfirmed,
 | 
			
		||||
                                      start__lt = date)
 | 
			
		||||
    def clean(date):
 | 
			
		||||
        qs = Diffusion.objects.filter(type=Diffusion.Type.unconfirmed,
 | 
			
		||||
                                      start__lt=date)
 | 
			
		||||
        logger.info('[clean] %d diffusions will be removed', qs.count())
 | 
			
		||||
        qs.delete()
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def check(date):
 | 
			
		||||
        qs = Diffusion.objects.filter(type = Diffusion.Type.unconfirmed,
 | 
			
		||||
                                      start__gt = date)
 | 
			
		||||
        qs = Diffusion.objects.filter(type=Diffusion.Type.unconfirmed,
 | 
			
		||||
                                      start__gt=date)
 | 
			
		||||
        items = []
 | 
			
		||||
        for diffusion in qs:
 | 
			
		||||
            schedules = Schedule.objects.filter(program = diffusion.program)
 | 
			
		||||
            schedules = Schedule.objects.filter(program=diffusion.program)
 | 
			
		||||
            for schedule in schedules:
 | 
			
		||||
                if schedule.match(diffusion.start):
 | 
			
		||||
                    break
 | 
			
		||||
@ -80,14 +80,14 @@ class Actions:
 | 
			
		||||
 | 
			
		||||
        logger.info('[check] %d diffusions will be removed', len(items))
 | 
			
		||||
        if len(items):
 | 
			
		||||
            Diffusion.objects.filter(id__in = items).delete()
 | 
			
		||||
            Diffusion.objects.filter(id__in=items).delete()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help= __doc__
 | 
			
		||||
    help = __doc__
 | 
			
		||||
 | 
			
		||||
    def add_arguments (self, parser):
 | 
			
		||||
        parser.formatter_class=RawTextHelpFormatter
 | 
			
		||||
    def add_arguments(self, parser):
 | 
			
		||||
        parser.formatter_class = RawTextHelpFormatter
 | 
			
		||||
        now = tz.datetime.today()
 | 
			
		||||
 | 
			
		||||
        group = parser.add_argument_group('action')
 | 
			
		||||
@ -130,23 +130,22 @@ class Command(BaseCommand):
 | 
			
		||||
                 'diffusions except those that conflicts with others'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def handle (self, *args, **options):
 | 
			
		||||
        date = tz.datetime(year = options.get('year'),
 | 
			
		||||
                                 month = options.get('month'),
 | 
			
		||||
                                 day = 1)
 | 
			
		||||
    def handle(self, *args, **options):
 | 
			
		||||
        date = tz.datetime(year=options.get('year'),
 | 
			
		||||
                           month=options.get('month'),
 | 
			
		||||
                           day=1)
 | 
			
		||||
        date = tz.make_aware(date)
 | 
			
		||||
        if options.get('next_month'):
 | 
			
		||||
            month = options.get('month')
 | 
			
		||||
            date += tz.timedelta(days = 28)
 | 
			
		||||
            date += tz.timedelta(days=28)
 | 
			
		||||
            if date.month == month:
 | 
			
		||||
                date += tz.timedelta(days = 28)
 | 
			
		||||
                date += tz.timedelta(days=28)
 | 
			
		||||
 | 
			
		||||
            date = date.replace(day = 1)
 | 
			
		||||
            date = date.replace(day=1)
 | 
			
		||||
 | 
			
		||||
        if options.get('update'):
 | 
			
		||||
            Actions.update(date, mode = options.get('mode'))
 | 
			
		||||
            Actions.update(date, mode=options.get('mode'))
 | 
			
		||||
        if options.get('clean'):
 | 
			
		||||
            Actions.clean(date)
 | 
			
		||||
        if options.get('check'):
 | 
			
		||||
            Actions.check(date)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,7 @@ from django.contrib.contenttypes.models import ContentType
 | 
			
		||||
 | 
			
		||||
from aircox.models import *
 | 
			
		||||
import aircox.settings as settings
 | 
			
		||||
__doc__ = __doc__.format(settings = settings)
 | 
			
		||||
__doc__ = __doc__.format(settings=settings)
 | 
			
		||||
 | 
			
		||||
logger = logging.getLogger('aircox.tools')
 | 
			
		||||
 | 
			
		||||
@ -78,8 +78,8 @@ class Importer:
 | 
			
		||||
                return
 | 
			
		||||
            try:
 | 
			
		||||
                timestamp = int(line.get('minutes') or 0) * 60 + \
 | 
			
		||||
                            int(line.get('seconds') or 0) \
 | 
			
		||||
                            if has_timestamp else None
 | 
			
		||||
                    int(line.get('seconds') or 0) \
 | 
			
		||||
                    if has_timestamp else None
 | 
			
		||||
 | 
			
		||||
                track, created = Track.objects.get_or_create(
 | 
			
		||||
                    title=line.get('title'),
 | 
			
		||||
@ -88,6 +88,7 @@ class Importer:
 | 
			
		||||
                    **self.track_kwargs
 | 
			
		||||
                )
 | 
			
		||||
                track.timestamp = timestamp
 | 
			
		||||
                print('track', track, timestamp)
 | 
			
		||||
                track.info = line.get('info')
 | 
			
		||||
                tags = line.get('tags')
 | 
			
		||||
                if tags:
 | 
			
		||||
@ -96,7 +97,7 @@ class Importer:
 | 
			
		||||
                logger.warning(
 | 
			
		||||
                    'an error occured for track {index}, it may not '
 | 
			
		||||
                    'have been saved: {err}'
 | 
			
		||||
                    .format(index = index, err=err)
 | 
			
		||||
                    .format(index=index, err=err)
 | 
			
		||||
                )
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
@ -107,10 +108,10 @@ class Importer:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command (BaseCommand):
 | 
			
		||||
    help= __doc__
 | 
			
		||||
    help = __doc__
 | 
			
		||||
 | 
			
		||||
    def add_arguments(self, parser):
 | 
			
		||||
        parser.formatter_class=RawTextHelpFormatter
 | 
			
		||||
        parser.formatter_class = RawTextHelpFormatter
 | 
			
		||||
        parser.add_argument(
 | 
			
		||||
            'path', metavar='PATH', type=str,
 | 
			
		||||
            help='path of the input playlist to read'
 | 
			
		||||
@ -125,7 +126,7 @@ class Command (BaseCommand):
 | 
			
		||||
            help='try to get the diffusion relative to the sound if it exists'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def handle (self, path, *args, **options):
 | 
			
		||||
    def handle(self, path, *args, **options):
 | 
			
		||||
        # FIXME: absolute/relative path of sounds vs given path
 | 
			
		||||
        if options.get('sound'):
 | 
			
		||||
            sound = Sound.objects.filter(
 | 
			
		||||
@ -136,7 +137,7 @@ class Command (BaseCommand):
 | 
			
		||||
            sound = Sound.objects.filter(path__icontains=path_).first()
 | 
			
		||||
 | 
			
		||||
        if not sound:
 | 
			
		||||
            logger.error('no sound found in the database for the path ' \
 | 
			
		||||
            logger.error('no sound found in the database for the path '
 | 
			
		||||
                         '{path}'.format(path=path))
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
@ -148,4 +149,3 @@ class Command (BaseCommand):
 | 
			
		||||
            logger.info('track #{pos} imported: {title}, by {artist}'.format(
 | 
			
		||||
                pos=track.position, title=track.title, artist=track.artist
 | 
			
		||||
            ))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -11,6 +11,7 @@ from django.core.management.base import BaseCommand, CommandError
 | 
			
		||||
 | 
			
		||||
logger = logging.getLogger('aircox.tools')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Stats:
 | 
			
		||||
    attributes = [
 | 
			
		||||
        'DC offset', 'Min level', 'Max level',
 | 
			
		||||
@ -18,7 +19,7 @@ class Stats:
 | 
			
		||||
        'RMS Tr dB', 'Flat factor', 'Length s',
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    def __init__ (self, path, **kwargs):
 | 
			
		||||
    def __init__(self, path, **kwargs):
 | 
			
		||||
        """
 | 
			
		||||
        If path is given, call analyse with path and kwargs
 | 
			
		||||
        """
 | 
			
		||||
@ -26,10 +27,10 @@ class Stats:
 | 
			
		||||
        if path:
 | 
			
		||||
            self.analyse(path, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def get (self, attr):
 | 
			
		||||
    def get(self, attr):
 | 
			
		||||
        return self.values.get(attr)
 | 
			
		||||
 | 
			
		||||
    def parse (self, output):
 | 
			
		||||
    def parse(self, output):
 | 
			
		||||
        for attr in Stats.attributes:
 | 
			
		||||
            value = re.search(attr + r'\s+(?P<value>\S+)', output)
 | 
			
		||||
            value = value and value.groupdict()
 | 
			
		||||
@ -41,14 +42,14 @@ class Stats:
 | 
			
		||||
                self.values[attr] = value
 | 
			
		||||
        self.values['length'] = self.values['Length s']
 | 
			
		||||
 | 
			
		||||
    def analyse (self, path, at = None, length = None):
 | 
			
		||||
    def analyse(self, path, at=None, length=None):
 | 
			
		||||
        """
 | 
			
		||||
        If at and length are given use them as excerpt to analyse.
 | 
			
		||||
        """
 | 
			
		||||
        args = ['sox', path, '-n']
 | 
			
		||||
 | 
			
		||||
        if at is not None and length is not None:
 | 
			
		||||
            args += ['trim', str(at), str(length) ]
 | 
			
		||||
            args += ['trim', str(at), str(length)]
 | 
			
		||||
 | 
			
		||||
        args.append('stats')
 | 
			
		||||
 | 
			
		||||
@ -66,17 +67,17 @@ class Sound:
 | 
			
		||||
    bad = None              # list of bad samples
 | 
			
		||||
    good = None             # list of good samples
 | 
			
		||||
 | 
			
		||||
    def __init__ (self, path, sample_length = None):
 | 
			
		||||
    def __init__(self, path, sample_length=None):
 | 
			
		||||
        self.path = path
 | 
			
		||||
        self.sample_length = sample_length if sample_length is not None \
 | 
			
		||||
                                else self.sample_length
 | 
			
		||||
            else self.sample_length
 | 
			
		||||
 | 
			
		||||
    def get_file_stats (self):
 | 
			
		||||
    def get_file_stats(self):
 | 
			
		||||
        return self.stats and self.stats[0]
 | 
			
		||||
 | 
			
		||||
    def analyse (self):
 | 
			
		||||
    def analyse(self):
 | 
			
		||||
        logger.info('complete file analysis')
 | 
			
		||||
        self.stats = [ Stats(self.path) ]
 | 
			
		||||
        self.stats = [Stats(self.path)]
 | 
			
		||||
        position = 0
 | 
			
		||||
        length = self.stats[0].get('length')
 | 
			
		||||
 | 
			
		||||
@ -85,21 +86,22 @@ class Sound:
 | 
			
		||||
 | 
			
		||||
        logger.info('start samples analysis...')
 | 
			
		||||
        while position < length:
 | 
			
		||||
            stats = Stats(self.path, at = position, length = self.sample_length)
 | 
			
		||||
            stats = Stats(self.path, at=position, length=self.sample_length)
 | 
			
		||||
            self.stats.append(stats)
 | 
			
		||||
            position += self.sample_length
 | 
			
		||||
 | 
			
		||||
    def check (self, name, min_val, max_val):
 | 
			
		||||
        self.good = [ index for index, stats in enumerate(self.stats)
 | 
			
		||||
                      if min_val <= stats.get(name) <= max_val ]
 | 
			
		||||
        self.bad = [ index for index, stats in enumerate(self.stats)
 | 
			
		||||
                      if index not in self.good ]
 | 
			
		||||
    def check(self, name, min_val, max_val):
 | 
			
		||||
        self.good = [index for index, stats in enumerate(self.stats)
 | 
			
		||||
                     if min_val <= stats.get(name) <= max_val]
 | 
			
		||||
        self.bad = [index for index, stats in enumerate(self.stats)
 | 
			
		||||
                    if index not in self.good]
 | 
			
		||||
        self.resume()
 | 
			
		||||
 | 
			
		||||
    def resume (self):
 | 
			
		||||
        view = lambda array: [
 | 
			
		||||
    def resume(self):
 | 
			
		||||
        def view(array): return [
 | 
			
		||||
            'file' if index is 0 else
 | 
			
		||||
            'sample {} (at {} seconds)'.format(index, (index-1) * self.sample_length)
 | 
			
		||||
            'sample {} (at {} seconds)'.format(
 | 
			
		||||
                index, (index-1) * self.sample_length)
 | 
			
		||||
            for index in array
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
@ -110,12 +112,13 @@ class Sound:
 | 
			
		||||
            logger.info(self.path + ' -> bad: \033[91m%s\033[0m',
 | 
			
		||||
                        ', '.join(view(self.bad)))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command (BaseCommand):
 | 
			
		||||
    help = __doc__
 | 
			
		||||
    sounds = None
 | 
			
		||||
 | 
			
		||||
    def add_arguments (self, parser):
 | 
			
		||||
        parser.formatter_class=RawTextHelpFormatter
 | 
			
		||||
    def add_arguments(self, parser):
 | 
			
		||||
        parser.formatter_class = RawTextHelpFormatter
 | 
			
		||||
 | 
			
		||||
        parser.add_argument(
 | 
			
		||||
            'files', metavar='FILE', type=str, nargs='+',
 | 
			
		||||
@ -128,12 +131,12 @@ class Command (BaseCommand):
 | 
			
		||||
        )
 | 
			
		||||
        parser.add_argument(
 | 
			
		||||
            '-a', '--attribute', type=str,
 | 
			
		||||
            help='attribute name to use to check, that can be:\n' + \
 | 
			
		||||
                 ', '.join([ '"{}"'.format(attr) for attr in Stats.attributes ])
 | 
			
		||||
            help='attribute name to use to check, that can be:\n' +
 | 
			
		||||
                 ', '.join(['"{}"'.format(attr) for attr in Stats.attributes])
 | 
			
		||||
        )
 | 
			
		||||
        parser.add_argument(
 | 
			
		||||
            '-r', '--range', type=float, nargs=2,
 | 
			
		||||
            help='range of minimal and maximal accepted value such as: ' \
 | 
			
		||||
            help='range of minimal and maximal accepted value such as: '
 | 
			
		||||
                 '--range min max'
 | 
			
		||||
        )
 | 
			
		||||
        parser.add_argument(
 | 
			
		||||
@ -141,7 +144,7 @@ class Command (BaseCommand):
 | 
			
		||||
            help='print a resume of good and bad files'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def handle (self, *args, **options):
 | 
			
		||||
    def handle(self, *args, **options):
 | 
			
		||||
        # parameters
 | 
			
		||||
        minmax = options.get('range')
 | 
			
		||||
        if not minmax:
 | 
			
		||||
@ -152,8 +155,8 @@ class Command (BaseCommand):
 | 
			
		||||
            raise CommandError('no attribute specified')
 | 
			
		||||
 | 
			
		||||
        # sound analyse and checks
 | 
			
		||||
        self.sounds = [ Sound(path, options.get('sample_length'))
 | 
			
		||||
                        for path in options.get('files') ]
 | 
			
		||||
        self.sounds = [Sound(path, options.get('sample_length'))
 | 
			
		||||
                       for path in options.get('files')]
 | 
			
		||||
        self.bad = []
 | 
			
		||||
        self.good = []
 | 
			
		||||
        for sound in self.sounds:
 | 
			
		||||
@ -171,4 +174,3 @@ class Command (BaseCommand):
 | 
			
		||||
                logger.info('\033[92m+ %s\033[0m', sound.path)
 | 
			
		||||
            for sound in self.bad:
 | 
			
		||||
                logger.info('\033[91m+ %s\033[0m', sound.path)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,7 @@ used to:
 | 
			
		||||
- cancels Diffusions that have an archive but could not have been played;
 | 
			
		||||
- run Liquidsoap
 | 
			
		||||
"""
 | 
			
		||||
import tzlocal
 | 
			
		||||
import time
 | 
			
		||||
import re
 | 
			
		||||
 | 
			
		||||
@ -28,7 +29,6 @@ tz.activate(pytz.UTC)
 | 
			
		||||
# FIXME liquidsoap does not manage timezones -- we have to convert
 | 
			
		||||
#       'on_air' metadata we get from it into utc one in order to work
 | 
			
		||||
#       correctly.
 | 
			
		||||
import tzlocal
 | 
			
		||||
local_tz = tzlocal.get_localzone()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -118,10 +118,12 @@ class Monitor:
 | 
			
		||||
        self.handle()
 | 
			
		||||
 | 
			
		||||
    def log(self, date=None, **kwargs):
 | 
			
		||||
        """
 | 
			
		||||
        Create a log using **kwargs, and print info
 | 
			
		||||
        """
 | 
			
		||||
        """ Create a log using **kwargs, and print info """
 | 
			
		||||
        log = Log(station=self.station, date=date or tz.now(), **kwargs)
 | 
			
		||||
        if log.type == Log.Type.on_air and log.diffusion is None:
 | 
			
		||||
            log.collision = Diffusion.objects.station(log.station) \
 | 
			
		||||
                                     .on_air().at(log.date).first()
 | 
			
		||||
 | 
			
		||||
        log.save()
 | 
			
		||||
        log.print()
 | 
			
		||||
        return log
 | 
			
		||||
@ -153,7 +155,7 @@ class Monitor:
 | 
			
		||||
            # check for reruns
 | 
			
		||||
            if not diff.is_date_in_range(air_time) and not diff.initial:
 | 
			
		||||
                diff = Diffusion.objects.at(air_time) \
 | 
			
		||||
                                .filter(initial=diff).first()
 | 
			
		||||
                                .on_air().filter(initial=diff).first()
 | 
			
		||||
 | 
			
		||||
        # log sound on air
 | 
			
		||||
        return self.log(
 | 
			
		||||
@ -170,14 +172,14 @@ class Monitor:
 | 
			
		||||
        if log.diffusion:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        tracks = Track.objects.filter(sound=log.sound, timestamp_isnull=False)
 | 
			
		||||
        tracks = Track.objects.filter(sound=log.sound, timestamp__isnull=False)
 | 
			
		||||
        if not tracks.exists():
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        tracks = tracks.exclude(log__station=self.station, log__pk__gt=log.pk)
 | 
			
		||||
        now = tz.now()
 | 
			
		||||
        for track in tracks:
 | 
			
		||||
            pos = log.date + tz.timedelta(seconds=track.position)
 | 
			
		||||
            pos = log.date + tz.timedelta(seconds=track.timestamp)
 | 
			
		||||
            if pos > now:
 | 
			
		||||
                break
 | 
			
		||||
            # log track on air
 | 
			
		||||
@ -195,14 +197,14 @@ class Monitor:
 | 
			
		||||
        if self.sync_next and self.sync_next < now:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        self.sync_next = now + tz.timedelta(seconds = self.sync_timeout)
 | 
			
		||||
        self.sync_next = now + tz.timedelta(seconds=self.sync_timeout)
 | 
			
		||||
 | 
			
		||||
        for source in self.station.sources:
 | 
			
		||||
            if source == self.station.dealer:
 | 
			
		||||
                continue
 | 
			
		||||
            playlist = source.program.sound_set.all() \
 | 
			
		||||
                             .filter(type=Sound.Type.archive) \
 | 
			
		||||
                             .values_list('path', flat = True)
 | 
			
		||||
                             .values_list('path', flat=True)
 | 
			
		||||
            source.playlist = list(playlist)
 | 
			
		||||
 | 
			
		||||
    def trace_canceled(self):
 | 
			
		||||
@ -214,24 +216,24 @@ class Monitor:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        qs = Diffusions.objects.station(self.station).at().filter(
 | 
			
		||||
            type = Diffusion.Type.normal,
 | 
			
		||||
            sound__type = Sound.Type.archive,
 | 
			
		||||
            type=Diffusion.Type.normal,
 | 
			
		||||
            sound__type=Sound.Type.archive,
 | 
			
		||||
        )
 | 
			
		||||
        logs = Log.objects.station(station).on_air().with_diff()
 | 
			
		||||
 | 
			
		||||
        date = tz.now() - datetime.timedelta(seconds = self.cancel_timeout)
 | 
			
		||||
        date = tz.now() - datetime.timedelta(seconds=self.cancel_timeout)
 | 
			
		||||
        for diff in qs:
 | 
			
		||||
            if logs.filter(diffusion = diff):
 | 
			
		||||
            if logs.filter(diffusion=diff):
 | 
			
		||||
                continue
 | 
			
		||||
            if diff.start < now:
 | 
			
		||||
                diff.type = Diffusion.Type.canceled
 | 
			
		||||
                diff.save()
 | 
			
		||||
                # log canceled diffusion
 | 
			
		||||
                self.log(
 | 
			
		||||
                    type = Log.Type.other,
 | 
			
		||||
                    diffusion = diff,
 | 
			
		||||
                    comment = 'Diffusion canceled after {} seconds' \
 | 
			
		||||
                              .format(self.cancel_timeout)
 | 
			
		||||
                    type=Log.Type.other,
 | 
			
		||||
                    diffusion=diff,
 | 
			
		||||
                    comment='Diffusion canceled after {} seconds'
 | 
			
		||||
                    .format(self.cancel_timeout)
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
    def __current_diff(self):
 | 
			
		||||
@ -251,7 +253,7 @@ class Monitor:
 | 
			
		||||
 | 
			
		||||
        # last sound source change: end of file reached or forced to stop
 | 
			
		||||
        sounds = Log.objects.station(station).on_air().with_sound() \
 | 
			
		||||
                            .filter(date__gte = log.date) \
 | 
			
		||||
                            .filter(date__gte=log.date) \
 | 
			
		||||
                            .order_by('date')
 | 
			
		||||
 | 
			
		||||
        if sounds.count() and sounds.last().source != log.source:
 | 
			
		||||
@ -259,12 +261,12 @@ class Monitor:
 | 
			
		||||
 | 
			
		||||
        # last diff is still playing: get remaining playlist
 | 
			
		||||
        sounds = sounds \
 | 
			
		||||
            .filter(source = log.source, pk__gt = log.pk) \
 | 
			
		||||
            .exclude(sound__type = Sound.Type.removed)
 | 
			
		||||
            .filter(source=log.source, pk__gt=log.pk) \
 | 
			
		||||
            .exclude(sound__type=Sound.Type.removed)
 | 
			
		||||
 | 
			
		||||
        remaining = log.diffusion.get_sounds(archive = True) \
 | 
			
		||||
                       .exclude(pk__in = sounds) \
 | 
			
		||||
                       .values_list('path', flat = True)
 | 
			
		||||
        remaining = log.diffusion.get_sounds(archive=True) \
 | 
			
		||||
                       .exclude(pk__in=sounds) \
 | 
			
		||||
                       .values_list('path', flat=True)
 | 
			
		||||
        return log.diffusion, list(remaining)
 | 
			
		||||
 | 
			
		||||
    def __next_diff(self, diff):
 | 
			
		||||
@ -273,16 +275,14 @@ class Monitor:
 | 
			
		||||
        If diff is given, it is the one to be played right after it.
 | 
			
		||||
        """
 | 
			
		||||
        station = self.station
 | 
			
		||||
 | 
			
		||||
        kwargs = {'start__gte': diff.end } if diff else {}
 | 
			
		||||
        kwargs['type'] = Diffusion.Type.normal
 | 
			
		||||
 | 
			
		||||
        qs = Diffusion.objects.station(station).at().filter(**kwargs) \
 | 
			
		||||
        kwargs = {'start__gte': diff.end} if diff else {}
 | 
			
		||||
        qs = Diffusion.objects.station(station) \
 | 
			
		||||
                      .on_air().at().filter(**kwargs) \
 | 
			
		||||
                      .distinct().order_by('start')
 | 
			
		||||
        diff = qs.first()
 | 
			
		||||
        return (diff, diff and diff.get_playlist(archive = True) or [])
 | 
			
		||||
        return (diff, diff and diff.get_playlist(archive=True) or [])
 | 
			
		||||
 | 
			
		||||
    def handle_pl_sync(self, source, playlist, diff = None, date = None):
 | 
			
		||||
    def handle_pl_sync(self, source, playlist, diff=None, date=None):
 | 
			
		||||
        """
 | 
			
		||||
        Update playlist of a source if required, and handle logging when
 | 
			
		||||
        it is needed.
 | 
			
		||||
@ -297,11 +297,11 @@ class Monitor:
 | 
			
		||||
        source.playlist = playlist
 | 
			
		||||
        if diff and not diff.is_live():
 | 
			
		||||
            # log diffusion archive load
 | 
			
		||||
            self.log(type = Log.Type.load,
 | 
			
		||||
                     source = source.id,
 | 
			
		||||
                     diffusion = diff,
 | 
			
		||||
                     date = date,
 | 
			
		||||
                     comment = '\n'.join(playlist))
 | 
			
		||||
            self.log(type=Log.Type.load,
 | 
			
		||||
                     source=source.id,
 | 
			
		||||
                     diffusion=diff,
 | 
			
		||||
                     date=date,
 | 
			
		||||
                     comment='\n'.join(playlist))
 | 
			
		||||
 | 
			
		||||
    def handle_diff_start(self, source, diff, date):
 | 
			
		||||
        """
 | 
			
		||||
@ -318,11 +318,11 @@ class Monitor:
 | 
			
		||||
        # live: just log it
 | 
			
		||||
        if diff.is_live():
 | 
			
		||||
            diff_ = Log.objects.station(self.station) \
 | 
			
		||||
                       .filter(diffusion = diff, type = Log.Type.on_air)
 | 
			
		||||
                       .filter(diffusion=diff, type=Log.Type.on_air)
 | 
			
		||||
            if not diff_.count():
 | 
			
		||||
                # log live diffusion
 | 
			
		||||
                self.log(type = Log.Type.on_air, source = source.id,
 | 
			
		||||
                         diffusion = diff, date = date)
 | 
			
		||||
                self.log(type=Log.Type.on_air, source=source.id,
 | 
			
		||||
                         diffusion=diff, date=date)
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        # enable dealer
 | 
			
		||||
@ -331,8 +331,8 @@ class Monitor:
 | 
			
		||||
            last_start = self.last_diff_start
 | 
			
		||||
            if not last_start or last_start.diffusion_id != diff.pk:
 | 
			
		||||
                # log triggered diffusion
 | 
			
		||||
                self.log(type = Log.Type.start, source = source.id,
 | 
			
		||||
                         diffusion = diff, date = date)
 | 
			
		||||
                self.log(type=Log.Type.start, source=source.id,
 | 
			
		||||
                         diffusion=diff, date=date)
 | 
			
		||||
 | 
			
		||||
    def handle(self):
 | 
			
		||||
        """
 | 
			
		||||
@ -358,10 +358,10 @@ class Monitor:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command (BaseCommand):
 | 
			
		||||
    help= __doc__
 | 
			
		||||
    help = __doc__
 | 
			
		||||
 | 
			
		||||
    def add_arguments (self, parser):
 | 
			
		||||
        parser.formatter_class=RawTextHelpFormatter
 | 
			
		||||
    def add_arguments(self, parser):
 | 
			
		||||
        parser.formatter_class = RawTextHelpFormatter
 | 
			
		||||
        group = parser.add_argument_group('actions')
 | 
			
		||||
        group.add_argument(
 | 
			
		||||
            '-c', '--config', action='store_true',
 | 
			
		||||
@ -396,25 +396,25 @@ class Command (BaseCommand):
 | 
			
		||||
                 'check'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def handle (self, *args,
 | 
			
		||||
                config = None, run = None, monitor = None,
 | 
			
		||||
                station = [], delay = 1000, timeout = 600,
 | 
			
		||||
                **options):
 | 
			
		||||
    def handle(self, *args,
 | 
			
		||||
               config=None, run=None, monitor=None,
 | 
			
		||||
               station=[], delay=1000, timeout=600,
 | 
			
		||||
               **options):
 | 
			
		||||
 | 
			
		||||
        stations = Station.objects.filter(name__in = station)[:] \
 | 
			
		||||
                    if station else Station.objects.all()[:]
 | 
			
		||||
        stations = Station.objects.filter(name__in=station)[:] \
 | 
			
		||||
            if station else Station.objects.all()[:]
 | 
			
		||||
 | 
			
		||||
        for station in stations:
 | 
			
		||||
            # station.prepare()
 | 
			
		||||
            if config and not run: # no need to write it twice
 | 
			
		||||
            if config and not run:  # no need to write it twice
 | 
			
		||||
                station.streamer.push()
 | 
			
		||||
            if run:
 | 
			
		||||
                station.streamer.process_run()
 | 
			
		||||
 | 
			
		||||
        if monitor:
 | 
			
		||||
            monitors = [
 | 
			
		||||
                Monitor(station, cancel_timeout = timeout)
 | 
			
		||||
                    for station in stations
 | 
			
		||||
                Monitor(station, cancel_timeout=timeout)
 | 
			
		||||
                for station in stations
 | 
			
		||||
            ]
 | 
			
		||||
            delay = delay / 1000
 | 
			
		||||
            while True:
 | 
			
		||||
@ -425,4 +425,3 @@ class Command (BaseCommand):
 | 
			
		||||
        if run:
 | 
			
		||||
            for station in stations:
 | 
			
		||||
                station.controller.process_wait()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										162
									
								
								aircox/models.py
									
									
									
									
									
								
							
							
						
						
									
										162
									
								
								aircox/models.py
									
									
									
									
									
								
							@ -11,6 +11,7 @@ from django.contrib.contenttypes.fields import (GenericForeignKey,
 | 
			
		||||
                                                GenericRelation)
 | 
			
		||||
from django.contrib.contenttypes.models import ContentType
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models import Q
 | 
			
		||||
from django.db.transaction import atomic
 | 
			
		||||
from django.template.defaultfilters import slugify
 | 
			
		||||
from django.utils import timezone as tz
 | 
			
		||||
@ -119,9 +120,7 @@ class Station(Nameable):
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def outputs(self):
 | 
			
		||||
        """
 | 
			
		||||
        Return all active output ports of the station
 | 
			
		||||
        """
 | 
			
		||||
        """ Return all active output ports of the station """
 | 
			
		||||
        return self.port_set.filter(
 | 
			
		||||
            direction=Port.Direction.output,
 | 
			
		||||
            active=True,
 | 
			
		||||
@ -129,9 +128,7 @@ class Station(Nameable):
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def sources(self):
 | 
			
		||||
        """
 | 
			
		||||
        Audio sources, dealer included
 | 
			
		||||
        """
 | 
			
		||||
        """ Audio sources, dealer included """
 | 
			
		||||
        self.__prepare_controls()
 | 
			
		||||
        return self.__sources
 | 
			
		||||
 | 
			
		||||
@ -168,7 +165,7 @@ class Station(Nameable):
 | 
			
		||||
 | 
			
		||||
        # FIXME can be a potential source of bug
 | 
			
		||||
        if date:
 | 
			
		||||
            date = utils.cast_date(date, to_datetime=False)
 | 
			
		||||
            date = utils.cast_date(date, datetime.date)
 | 
			
		||||
        if date and date > datetime.date.today():
 | 
			
		||||
            return []
 | 
			
		||||
 | 
			
		||||
@ -185,18 +182,17 @@ class Station(Nameable):
 | 
			
		||||
                                     start__lte=now) \
 | 
			
		||||
                             .order_by('-start')[:count]
 | 
			
		||||
 | 
			
		||||
        q = models.Q(diffusion__isnull=False) | \
 | 
			
		||||
            models.Q(track__isnull=False)
 | 
			
		||||
        q = Q(diffusion__isnull=False) | Q(track__isnull=False)
 | 
			
		||||
        logs = logs.station(self).on_air().filter(q).order_by('-date')
 | 
			
		||||
 | 
			
		||||
        # filter out tracks played when there was a diffusion
 | 
			
		||||
        n, q = 0, models.Q()
 | 
			
		||||
        n, q = 0, Q()
 | 
			
		||||
        for diff in diffs:
 | 
			
		||||
            if count and n >= count:
 | 
			
		||||
                break
 | 
			
		||||
            # FIXME: does not catch tracks started before diff end but
 | 
			
		||||
            #        that continued afterwards
 | 
			
		||||
            q = q | models.Q(date__gte=diff.start, date__lte=diff.end)
 | 
			
		||||
            q = q | Q(date__gte=diff.start, date__lte=diff.end)
 | 
			
		||||
            n += 1
 | 
			
		||||
        logs = logs.exclude(q, diffusion__isnull=True)
 | 
			
		||||
        if count:
 | 
			
		||||
@ -411,15 +407,11 @@ class Schedule(models.Model):
 | 
			
		||||
        Program, models.CASCADE,
 | 
			
		||||
        verbose_name=_('related program'),
 | 
			
		||||
    )
 | 
			
		||||
    time = models.TimeField(
 | 
			
		||||
        _('time'),
 | 
			
		||||
        blank=True, null=True,
 | 
			
		||||
        help_text=_('start time'),
 | 
			
		||||
    )
 | 
			
		||||
    date = models.DateField(
 | 
			
		||||
        _('date'),
 | 
			
		||||
        blank=True, null=True,
 | 
			
		||||
        help_text=_('date of the first diffusion'),
 | 
			
		||||
        _('date'), help_text=_('date of the first diffusion'),
 | 
			
		||||
    )
 | 
			
		||||
    time = models.TimeField(
 | 
			
		||||
        _('time'), help_text=_('start time'),
 | 
			
		||||
    )
 | 
			
		||||
    timezone = models.CharField(
 | 
			
		||||
        _('timezone'),
 | 
			
		||||
@ -462,6 +454,12 @@ class Schedule(models.Model):
 | 
			
		||||
 | 
			
		||||
        return pytz.timezone(self.timezone)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def datetime(self):
 | 
			
		||||
        """ Datetime for this schedule (timezone unaware) """
 | 
			
		||||
        import datetime
 | 
			
		||||
        return datetime.datetime.combine(self.date, self.time)
 | 
			
		||||
 | 
			
		||||
    # initial cached data
 | 
			
		||||
    __initial = None
 | 
			
		||||
 | 
			
		||||
@ -481,22 +479,18 @@ class Schedule(models.Model):
 | 
			
		||||
 | 
			
		||||
    def match(self, date=None, check_time=True):
 | 
			
		||||
        """
 | 
			
		||||
        Return True if the given datetime matches the schedule
 | 
			
		||||
        Return True if the given date(time) matches the schedule.
 | 
			
		||||
        """
 | 
			
		||||
        date = utils.date_or_default(date)
 | 
			
		||||
        date = utils.date_or_default(
 | 
			
		||||
            date, tz.datetime if check_time else datetime.date)
 | 
			
		||||
 | 
			
		||||
        if self.date.weekday() != date.weekday() or \
 | 
			
		||||
                not self.match_week(date):
 | 
			
		||||
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        if not check_time:
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        # we check against a normalized version (norm_date will have
 | 
			
		||||
        # schedule's date.
 | 
			
		||||
 | 
			
		||||
        return date == self.normalize(date)
 | 
			
		||||
        return date == self.normalize(date) if check_time else True
 | 
			
		||||
 | 
			
		||||
    def match_week(self, date=None):
 | 
			
		||||
        """
 | 
			
		||||
@ -509,15 +503,14 @@ class Schedule(models.Model):
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        # since we care only about the week, go to the same day of the week
 | 
			
		||||
        date = utils.date_or_default(date)
 | 
			
		||||
        date = utils.date_or_default(date, datetime.date)
 | 
			
		||||
        date += tz.timedelta(days=self.date.weekday() - date.weekday())
 | 
			
		||||
 | 
			
		||||
        # FIXME this case
 | 
			
		||||
 | 
			
		||||
        if self.frequency == Schedule.Frequency.one_on_two:
 | 
			
		||||
            # cf notes in date_of_month
 | 
			
		||||
            diff = utils.cast_date(date, False) - \
 | 
			
		||||
                utils.cast_date(self.date, False)
 | 
			
		||||
            diff = date - utils.cast_date(self.date, datetime.date)
 | 
			
		||||
 | 
			
		||||
            return not (diff.days % 14)
 | 
			
		||||
 | 
			
		||||
@ -556,8 +549,7 @@ class Schedule(models.Model):
 | 
			
		||||
            return []
 | 
			
		||||
 | 
			
		||||
        # first day of month
 | 
			
		||||
        date = utils.date_or_default(date, to_datetime=False) \
 | 
			
		||||
                    .replace(day=1)
 | 
			
		||||
        date = utils.date_or_default(date, datetime.date).replace(day=1)
 | 
			
		||||
        freq = self.frequency
 | 
			
		||||
 | 
			
		||||
        # last of the month
 | 
			
		||||
@ -588,8 +580,8 @@ class Schedule(models.Model):
 | 
			
		||||
 | 
			
		||||
        if freq == Schedule.Frequency.one_on_two:
 | 
			
		||||
            # check date base on a diff of dates base on a 14 days delta
 | 
			
		||||
            diff = utils.cast_date(date, False) - \
 | 
			
		||||
                utils.cast_date(self.date, False)
 | 
			
		||||
            diff = utils.cast_date(date, datetime.date) - \
 | 
			
		||||
                utils.cast_date(self.date, datetime.date)
 | 
			
		||||
 | 
			
		||||
            if diff.days % 14:
 | 
			
		||||
                date += tz.timedelta(days=7)
 | 
			
		||||
@ -636,13 +628,17 @@ class Schedule(models.Model):
 | 
			
		||||
        # new diffusions
 | 
			
		||||
        duration = utils.to_timedelta(self.duration)
 | 
			
		||||
 | 
			
		||||
        delta = None
 | 
			
		||||
        if self.initial:
 | 
			
		||||
            delta = self.date - self.initial.date
 | 
			
		||||
            delta = self.datetime - self.initial.datetime
 | 
			
		||||
 | 
			
		||||
        # FIXME: daylight saving bug: delta misses an hour when diffusion and
 | 
			
		||||
        #        rerun are not on the same daylight-saving timezone
 | 
			
		||||
        diffusions += [
 | 
			
		||||
            Diffusion(
 | 
			
		||||
                program=self.program,
 | 
			
		||||
                type=Diffusion.Type.unconfirmed,
 | 
			
		||||
                initial=Diffusion.objects.filter(start=date - delta).first()
 | 
			
		||||
                initial=Diffusion.objects.program(self.program).filter(start=date-delta).first()
 | 
			
		||||
                if self.initial else None,
 | 
			
		||||
                start=date,
 | 
			
		||||
                end=date + duration,
 | 
			
		||||
@ -685,7 +681,10 @@ class DiffusionQuerySet(models.QuerySet):
 | 
			
		||||
    def program(self, program):
 | 
			
		||||
        return self.filter(program=program)
 | 
			
		||||
 | 
			
		||||
    def at(self, date=None, next=False, **kwargs):
 | 
			
		||||
    def on_air(self):
 | 
			
		||||
        return self.filter(type=Diffusion.Type.normal)
 | 
			
		||||
 | 
			
		||||
    def at(self, date=None):
 | 
			
		||||
        """
 | 
			
		||||
        Return diffusions occuring at the given date, ordered by +start
 | 
			
		||||
 | 
			
		||||
@ -694,12 +693,9 @@ class DiffusionQuerySet(models.QuerySet):
 | 
			
		||||
        it as a date, and get diffusions that occurs this day.
 | 
			
		||||
 | 
			
		||||
        When date is None, uses tz.now().
 | 
			
		||||
 | 
			
		||||
        When next is true, include diffusions that also occur after
 | 
			
		||||
        the given moment.
 | 
			
		||||
        """
 | 
			
		||||
        # note: we work with localtime
 | 
			
		||||
        date = utils.date_or_default(date, keep_type=True)
 | 
			
		||||
        date = utils.date_or_default(date)
 | 
			
		||||
 | 
			
		||||
        qs = self
 | 
			
		||||
        filters = None
 | 
			
		||||
@ -708,43 +704,39 @@ class DiffusionQuerySet(models.QuerySet):
 | 
			
		||||
            # use datetime: we want diffusion that occurs around this
 | 
			
		||||
            # range
 | 
			
		||||
            filters = {'start__lte': date, 'end__gte': date}
 | 
			
		||||
 | 
			
		||||
            if next:
 | 
			
		||||
                qs = qs.filter(
 | 
			
		||||
                    models.Q(start__gte=date) | models.Q(**filters)
 | 
			
		||||
                )
 | 
			
		||||
            else:
 | 
			
		||||
                qs = qs.filter(**filters)
 | 
			
		||||
            qs = qs.filter(**filters)
 | 
			
		||||
        else:
 | 
			
		||||
            # use date: we want diffusions that occurs this day
 | 
			
		||||
            start, end = utils.date_range(date)
 | 
			
		||||
            filters = models.Q(start__gte=start, start__lte=end) | \
 | 
			
		||||
                models.Q(end__gt=start, end__lt=end)
 | 
			
		||||
 | 
			
		||||
            if next:
 | 
			
		||||
                # include also diffusions of the next day
 | 
			
		||||
                filters |= models.Q(start__gte=start)
 | 
			
		||||
            qs = qs.filter(filters, **kwargs)
 | 
			
		||||
 | 
			
		||||
            qs = qs.filter(Q(start__date=date) | Q(end__date=date))
 | 
			
		||||
        return qs.order_by('start').distinct()
 | 
			
		||||
 | 
			
		||||
    def after(self, date=None, **kwargs):
 | 
			
		||||
    def after(self, date=None):
 | 
			
		||||
        """
 | 
			
		||||
        Return a queryset of diffusions that happen after the given
 | 
			
		||||
        date.
 | 
			
		||||
        """
 | 
			
		||||
        date = utils.date_or_default(date, keep_type=True)
 | 
			
		||||
 | 
			
		||||
        return self.filter(start__gte=date, **kwargs).order_by('start')
 | 
			
		||||
 | 
			
		||||
    def before(self, date=None, **kwargs):
 | 
			
		||||
        """
 | 
			
		||||
        Return a queryset of diffusions that finish before the given
 | 
			
		||||
        date.
 | 
			
		||||
        date (default: today).
 | 
			
		||||
        """
 | 
			
		||||
        date = utils.date_or_default(date)
 | 
			
		||||
        if isinstance(date, tz.datetime):
 | 
			
		||||
            qs = self.filter(start__gte=date)
 | 
			
		||||
        else:
 | 
			
		||||
            qs = self.filter(start__date__gte=date)
 | 
			
		||||
        return qs.order_by('start')
 | 
			
		||||
 | 
			
		||||
        return self.filter(end__lte=date, **kwargs).order_by('start')
 | 
			
		||||
    def before(self, date=None):
 | 
			
		||||
        """
 | 
			
		||||
        Return a queryset of diffusions that finish before the given
 | 
			
		||||
        date (default: today).
 | 
			
		||||
        """
 | 
			
		||||
        date = utils.date_or_default(date)
 | 
			
		||||
        if isinstance(date, tz.datetime):
 | 
			
		||||
            qs = self.filter(start__lt=date)
 | 
			
		||||
        else:
 | 
			
		||||
            qs = self.filter(start__date__lt=date)
 | 
			
		||||
        return qs.order_by('start')
 | 
			
		||||
 | 
			
		||||
    def range(self, start, end):
 | 
			
		||||
        # FIXME can return dates that are out of range...
 | 
			
		||||
        return self.after(start).before(end)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Diffusion(models.Model):
 | 
			
		||||
@ -813,21 +805,19 @@ class Diffusion(models.Model):
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def date(self):
 | 
			
		||||
        """
 | 
			
		||||
        Alias to self.start
 | 
			
		||||
        """
 | 
			
		||||
        """ Return diffusion start as a date. """
 | 
			
		||||
 | 
			
		||||
        return self.start
 | 
			
		||||
        return utils.cast_date(self.start)
 | 
			
		||||
 | 
			
		||||
    @cached_property
 | 
			
		||||
    def local_date(self):
 | 
			
		||||
    def local_start(self):
 | 
			
		||||
        """
 | 
			
		||||
        Return a version of self.date that is localized to self.timezone;
 | 
			
		||||
        This is needed since datetime are stored as UTC date and we want
 | 
			
		||||
        to get it as local time.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        return tz.localtime(self.date, tz.get_current_timezone())
 | 
			
		||||
        return tz.localtime(self.start, tz.get_current_timezone())
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def local_end(self):
 | 
			
		||||
@ -892,10 +882,8 @@ class Diffusion(models.Model):
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        return Diffusion.objects.filter(
 | 
			
		||||
            models.Q(start__lt=self.start,
 | 
			
		||||
                     end__gt=self.start) |
 | 
			
		||||
            models.Q(start__gt=self.start,
 | 
			
		||||
                     start__lt=self.end)
 | 
			
		||||
            Q(start__lt=self.start, end__gt=self.start) |
 | 
			
		||||
            Q(start__gt=self.start, start__lt=self.end)
 | 
			
		||||
        ).exclude(pk=self.pk).distinct()
 | 
			
		||||
 | 
			
		||||
    def check_conflicts(self):
 | 
			
		||||
@ -929,7 +917,7 @@ class Diffusion(models.Model):
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return '{self.program.name} {date} #{self.pk}'.format(
 | 
			
		||||
            self=self, date=self.local_date.strftime('%Y/%m/%d %H:%M%z')
 | 
			
		||||
            self=self, date=self.local_start.strftime('%Y/%m/%d %H:%M%z')
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
@ -1298,11 +1286,8 @@ class LogQuerySet(models.QuerySet):
 | 
			
		||||
        return self.filter(station=station)
 | 
			
		||||
 | 
			
		||||
    def at(self, date=None):
 | 
			
		||||
        start, end = utils.date_range(date)
 | 
			
		||||
        # return qs.filter(models.Q(end__gte = start) |
 | 
			
		||||
        #                 models.Q(date__lte = end))
 | 
			
		||||
 | 
			
		||||
        return self.filter(date__gte=start, date__lte=end)
 | 
			
		||||
        date = utils.date_or_default(date)
 | 
			
		||||
        return self.filter(date__date=date)
 | 
			
		||||
 | 
			
		||||
    def on_air(self):
 | 
			
		||||
        return self.filter(type=Log.Type.on_air)
 | 
			
		||||
@ -1508,6 +1493,13 @@ class Log(models.Model):
 | 
			
		||||
        verbose_name=_('Track'),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    collision = models.ForeignKey(
 | 
			
		||||
        Diffusion, on_delete=models.SET_NULL,
 | 
			
		||||
        blank=True, null=True,
 | 
			
		||||
        verbose_name=_('Collision'),
 | 
			
		||||
        related_name='+',
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    objects = LogQuerySet.as_manager()
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
 | 
			
		||||
@ -4,68 +4,59 @@ import django.utils.timezone as tz
 | 
			
		||||
 | 
			
		||||
def date_range(date):
 | 
			
		||||
    """
 | 
			
		||||
    Return a range of datetime for a given day, such as:
 | 
			
		||||
        [date, 0:0:0:0; date, 23:59:59:999]
 | 
			
		||||
 | 
			
		||||
    Ensure timezone awareness.
 | 
			
		||||
    Return a datetime range for a given day, as:
 | 
			
		||||
    ```(date, 0:0:0:0; date, 23:59:59:999)```.
 | 
			
		||||
    """
 | 
			
		||||
    date = date_or_default(date)
 | 
			
		||||
    range = (
 | 
			
		||||
        date.replace(hour = 0, minute = 0, second = 0), \
 | 
			
		||||
        date.replace(hour = 23, minute = 59, second = 59, microsecond = 999)
 | 
			
		||||
    date = date_or_default(date, tz.datetime)
 | 
			
		||||
    return (
 | 
			
		||||
        date.replace(hour=0, minute=0, second=0),
 | 
			
		||||
        date.replace(hour=23, minute=59, second=59, microsecond=999)
 | 
			
		||||
    )
 | 
			
		||||
    return range
 | 
			
		||||
 | 
			
		||||
def cast_date(date, to_datetime = True):
 | 
			
		||||
 | 
			
		||||
def cast_date(date, into=datetime.date):
 | 
			
		||||
    """
 | 
			
		||||
    Given a date reset its time information and
 | 
			
		||||
    return it as a date or datetime object.
 | 
			
		||||
 | 
			
		||||
    Ensure timezone awareness.
 | 
			
		||||
    Cast a given date into the provided class' instance. Make datetime
 | 
			
		||||
    aware of timezone.
 | 
			
		||||
    """
 | 
			
		||||
    if to_datetime:
 | 
			
		||||
        return tz.make_aware(
 | 
			
		||||
            tz.datetime(date.year, date.month, date.day, 0, 0, 0, 0)
 | 
			
		||||
        )
 | 
			
		||||
    return datetime.date(date.year, date.month, date.day)
 | 
			
		||||
    date = into(date.year, date.month, date.day)
 | 
			
		||||
    return tz.make_aware(date) if issubclass(into, tz.datetime) else date
 | 
			
		||||
 | 
			
		||||
def date_or_default(date, reset_time = False, keep_type = False, to_datetime = True):
 | 
			
		||||
 | 
			
		||||
def date_or_default(date, into=None):
 | 
			
		||||
    """
 | 
			
		||||
    Return datetime or default value (now) if not defined, and remove time info
 | 
			
		||||
    if reset_time is True.
 | 
			
		||||
 | 
			
		||||
    \param reset_time   reset time info to 0
 | 
			
		||||
    \param keep_type    keep the same type of the given date if not None
 | 
			
		||||
    \param to_datetime  force conversion to datetime if not keep_type
 | 
			
		||||
 | 
			
		||||
    Ensure timezone awareness.
 | 
			
		||||
    Return date if not None, otherwise return now. Cast result into provided
 | 
			
		||||
    type if any.
 | 
			
		||||
    """
 | 
			
		||||
    date = date or tz.now()
 | 
			
		||||
    to_datetime = isinstance(date, tz.datetime) if keep_type else to_datetime
 | 
			
		||||
    date = date if date is not None else datetime.date.today() \
 | 
			
		||||
        if into is not None and issubclass(into, datetime.date) else \
 | 
			
		||||
        tz.datetime.now()
 | 
			
		||||
 | 
			
		||||
    if reset_time or not isinstance(date, tz.datetime):
 | 
			
		||||
        return cast_date(date, to_datetime)
 | 
			
		||||
    if into is not None:
 | 
			
		||||
        date = cast_date(date, into)
 | 
			
		||||
 | 
			
		||||
    if not tz.is_aware(date):
 | 
			
		||||
    if isinstance(date, tz.datetime) and not tz.is_aware(date):
 | 
			
		||||
        date = tz.make_aware(date)
 | 
			
		||||
    return date
 | 
			
		||||
 | 
			
		||||
def to_timedelta (time):
 | 
			
		||||
 | 
			
		||||
def to_timedelta(time):
 | 
			
		||||
    """
 | 
			
		||||
    Transform a datetime or a time instance to a timedelta,
 | 
			
		||||
    only using time info
 | 
			
		||||
    """
 | 
			
		||||
    return datetime.timedelta(
 | 
			
		||||
        hours = time.hour,
 | 
			
		||||
        minutes = time.minute,
 | 
			
		||||
        seconds = time.second
 | 
			
		||||
        hours=time.hour,
 | 
			
		||||
        minutes=time.minute,
 | 
			
		||||
        seconds=time.second
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
def seconds_to_time (seconds):
 | 
			
		||||
 | 
			
		||||
def seconds_to_time(seconds):
 | 
			
		||||
    """
 | 
			
		||||
    Seconds to datetime.time
 | 
			
		||||
    """
 | 
			
		||||
    minutes, seconds = divmod(seconds, 60)
 | 
			
		||||
    hours, minutes = divmod(minutes, 60)
 | 
			
		||||
    return datetime.time(hour = hours, minute = minutes, second = seconds)
 | 
			
		||||
    return datetime.time(hour=hours, minute=minutes, second=seconds)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -793,10 +793,10 @@ class TimetablePage(DatedListPage):
 | 
			
		||||
    template = 'aircox_cms/dated_list_page.html'
 | 
			
		||||
    station = models.ForeignKey(
 | 
			
		||||
        aircox.models.Station,
 | 
			
		||||
        verbose_name = _('station'),
 | 
			
		||||
        on_delete = models.SET_NULL,
 | 
			
		||||
        null = True, blank = True,
 | 
			
		||||
        help_text = _('(required) related station')
 | 
			
		||||
        verbose_name=_('station'),
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
        null=True, blank=True,
 | 
			
		||||
        help_text=_('(required) related station')
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    content_panels = DatedListPage.content_panels + [
 | 
			
		||||
 | 
			
		||||
@ -15,6 +15,8 @@ from aircox.admin.mixins import UnrelatedInlineMixin
 | 
			
		||||
 | 
			
		||||
@admin.register(models.Site)
 | 
			
		||||
class SiteAdmin(ContentEditor):
 | 
			
		||||
    list_display = ['title', 'station']
 | 
			
		||||
 | 
			
		||||
    inlines = [
 | 
			
		||||
        ContentEditorInline.create(models.SiteRichText),
 | 
			
		||||
        ContentEditorInline.create(models.SiteImage),
 | 
			
		||||
@ -37,14 +39,26 @@ class PageDiffusionPlaylist(UnrelatedInlineMixin, TracksInline):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(models.Page)
 | 
			
		||||
class PageAdmin(ContentEditor):
 | 
			
		||||
class PageAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ["title", "parent", "status"]
 | 
			
		||||
    list_editable = ['status']
 | 
			
		||||
    prepopulated_fields = {"slug": ("title",)}
 | 
			
		||||
    # readonly_fields = ('diffusion',)
 | 
			
		||||
 | 
			
		||||
    fieldsets = (
 | 
			
		||||
        (_('Main'), {
 | 
			
		||||
            'fields': ['title', 'slug', 'as_program', 'headline'],
 | 
			
		||||
            'fields': ['title', 'slug']
 | 
			
		||||
        }),
 | 
			
		||||
        (_('Settings'), {
 | 
			
		||||
            'fields': ['status', 'static_path', 'path'],
 | 
			
		||||
        }),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(models.Article)
 | 
			
		||||
class ArticleAdmin(ContentEditor, PageAdmin):
 | 
			
		||||
    fieldsets = (
 | 
			
		||||
        (_('Main'), {
 | 
			
		||||
            'fields': ['title', 'slug', 'as_program', 'cover', 'headline'],
 | 
			
		||||
            'classes': ('tabbed', 'uncollapse')
 | 
			
		||||
        }),
 | 
			
		||||
        (_('Settings'), {
 | 
			
		||||
@ -59,36 +73,27 @@ class PageAdmin(ContentEditor):
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    inlines = [
 | 
			
		||||
        ContentEditorInline.create(models.PageRichText),
 | 
			
		||||
        ContentEditorInline.create(models.PageImage),
 | 
			
		||||
        ContentEditorInline.create(models.ArticleRichText),
 | 
			
		||||
        ContentEditorInline.create(models.ArticleImage),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(models.DiffusionPage)
 | 
			
		||||
class DiffusionPageAdmin(PageAdmin):
 | 
			
		||||
    fieldsets = copy.deepcopy(PageAdmin.fieldsets)
 | 
			
		||||
class DiffusionPageAdmin(ArticleAdmin):
 | 
			
		||||
    fieldsets = copy.deepcopy(ArticleAdmin.fieldsets)
 | 
			
		||||
    fieldsets[1][1]['fields'].insert(0, 'diffusion')
 | 
			
		||||
 | 
			
		||||
    inlines = PageAdmin.inlines + [
 | 
			
		||||
        PageDiffusionPlaylist
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    # TODO: permissions
 | 
			
		||||
    #def get_inline_instances(self, request, obj=None):
 | 
			
		||||
    #    inlines = super().get_inline_instances(request, obj)
 | 
			
		||||
    #    if obj and obj.diffusion:
 | 
			
		||||
    #        inlines.insert(0, PageDiffusionPlaylist(self.model, self.admin_site))
 | 
			
		||||
    #    return inlines
 | 
			
		||||
    def get_inline_instances(self, request, obj=None):
 | 
			
		||||
        inlines = super().get_inline_instances(request, obj)
 | 
			
		||||
        if obj and obj.diffusion:
 | 
			
		||||
            inlines.insert(0, PageDiffusionPlaylist(self.model, self.admin_site))
 | 
			
		||||
        return inlines
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(models.ProgramPage)
 | 
			
		||||
class DiffusionPageAdmin(PageAdmin):
 | 
			
		||||
    fieldsets = copy.deepcopy(PageAdmin.fieldsets)
 | 
			
		||||
class ProgramPageAdmin(ArticleAdmin):
 | 
			
		||||
    fieldsets = copy.deepcopy(ArticleAdmin.fieldsets)
 | 
			
		||||
    fieldsets[1][1]['fields'].insert(0, 'program')
 | 
			
		||||
 | 
			
		||||
    inlines = PageAdmin.inlines + [
 | 
			
		||||
        PageDiffusionPlaylist
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,5 @@
 | 
			
		||||
import './js';
 | 
			
		||||
import './styles.scss';
 | 
			
		||||
import './noscript.scss';
 | 
			
		||||
import './vue';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -3,9 +3,12 @@ import Buefy from 'buefy';
 | 
			
		||||
 | 
			
		||||
Vue.use(Buefy);
 | 
			
		||||
 | 
			
		||||
var app = new Vue({
 | 
			
		||||
  el: '#app',
 | 
			
		||||
})
 | 
			
		||||
window.addEventListener('load', () => {
 | 
			
		||||
    var app = new Vue({
 | 
			
		||||
      el: '#app',
 | 
			
		||||
      delimiters: [ '[[', ']]' ],
 | 
			
		||||
    })
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -10,9 +10,12 @@ $body-background-color: $light;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.navbar.has-shadow {
 | 
			
		||||
    box-shadow: 0em 0.1em 0.5em rgba(0,0,0,0.1);
 | 
			
		||||
    box-shadow: 0em 0.05em 0.5em rgba(0,0,0,0.1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.navbar-brand img {
 | 
			
		||||
    min-height: 6em;
 | 
			
		||||
}
 | 
			
		||||
@ -20,5 +23,22 @@ $body-background-color: $light;
 | 
			
		||||
.navbar-menu .navbar-item:not(:last-child) {
 | 
			
		||||
    border-right: 1px $grey solid;
 | 
			
		||||
}
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/** page **/
 | 
			
		||||
img.cover {
 | 
			
		||||
    border: 0.2em black solid;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.headline {
 | 
			
		||||
    font-size: 1.2em;
 | 
			
		||||
    padding: 0.2em 0em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
img.cover {
 | 
			
		||||
    float: right;
 | 
			
		||||
    max-width: 40%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										11
									
								
								aircox_web/assets/vue/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								aircox_web/assets/vue/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,11 @@
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
 | 
			
		||||
import Tab from './tab.vue';
 | 
			
		||||
import Tabs from './tabs.vue';
 | 
			
		||||
 | 
			
		||||
Vue.component('a-tab', Tab);
 | 
			
		||||
Vue.component('a-tabs', Tabs);
 | 
			
		||||
 | 
			
		||||
export {Tab, Tabs};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										31
									
								
								aircox_web/assets/vue/tab.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								aircox_web/assets/vue/tab.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,31 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <li @click.prevent="onclick"
 | 
			
		||||
        :class="{'is-active': $parent.value == value}">
 | 
			
		||||
        <slot></slot>
 | 
			
		||||
    </li>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
    props: {
 | 
			
		||||
        value: { default: undefined },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    methods: {
 | 
			
		||||
        select() {
 | 
			
		||||
            this.$parent.selectTab(this);
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        onclick(event) {
 | 
			
		||||
            this.select();
 | 
			
		||||
            /*if(event.target.href != document.location)
 | 
			
		||||
                window.history.pushState(
 | 
			
		||||
                    { url: event.target.href },
 | 
			
		||||
                    event.target.innerText + ' - ' + document.title,
 | 
			
		||||
                    event.target.href
 | 
			
		||||
                ) */
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										45
									
								
								aircox_web/assets/vue/tabs.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								aircox_web/assets/vue/tabs.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,45 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div>
 | 
			
		||||
        <div class="tabs is-centered">
 | 
			
		||||
            <ul><slot name="tabs" :value="value" /></ul>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <slot :value="value"/>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
    props: {
 | 
			
		||||
        default: { default: null },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            value: this.default,
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    computed: {
 | 
			
		||||
        tab() {
 | 
			
		||||
            const vnode = this.$slots.default && this.$slots.default.find(
 | 
			
		||||
                elm => elm.child && elm.child.value == this.value
 | 
			
		||||
            );
 | 
			
		||||
            return vnode && vnode.child;
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    methods: {
 | 
			
		||||
        selectTab(tab) {
 | 
			
		||||
            const value = tab.value;
 | 
			
		||||
            if(this.value === value)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            this.value = value;
 | 
			
		||||
            this.$emit('select', {target: this, value: value, tab: tab});
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										49
									
								
								aircox_web/converters.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								aircox_web/converters.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,49 @@
 | 
			
		||||
import datetime
 | 
			
		||||
 | 
			
		||||
from django.utils.safestring import mark_safe
 | 
			
		||||
from django.urls.converters import StringConverter
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PagePathConverter(StringConverter):
 | 
			
		||||
    """ Match path for pages, including surrounding slashes. """
 | 
			
		||||
    regex = r'/?|([-_a-zA-Z0-9]+/)*?'
 | 
			
		||||
 | 
			
		||||
    def to_python(self, value):
 | 
			
		||||
        if not value or value[0] != '/':
 | 
			
		||||
            value = '/' + value
 | 
			
		||||
        if len(value) > 1 and value[-1] != '/':
 | 
			
		||||
            value = value + '/'
 | 
			
		||||
        return value
 | 
			
		||||
 | 
			
		||||
    def to_url(self, value):
 | 
			
		||||
        if value[0] == '/':
 | 
			
		||||
            value = value[1:]
 | 
			
		||||
        if value[-1] != '/':
 | 
			
		||||
            value = value + '/'
 | 
			
		||||
        return mark_safe(value)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#class WeekConverter:
 | 
			
		||||
#    """ Converter for date as YYYYY/WW """
 | 
			
		||||
#    regex = r'[0-9]{4}/[0-9]{2}/?'
 | 
			
		||||
#
 | 
			
		||||
#    def to_python(self, value):
 | 
			
		||||
#        value = value.split('/')
 | 
			
		||||
#        return datetime.date(int(value[0]), int(value[1]), int(value[2]))
 | 
			
		||||
#
 | 
			
		||||
#    def to_url(self, value):
 | 
			
		||||
#        return '{:04d}/{:02d}/'.format(*value.isocalendar())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DateConverter:
 | 
			
		||||
    """ Converter for date as YYYY/MM/DD """
 | 
			
		||||
    regex = r'[0-9]{4}/[0-9]{2}/[0-9]{2}/?'
 | 
			
		||||
 | 
			
		||||
    def to_python(self, value):
 | 
			
		||||
        value = value.split('/')
 | 
			
		||||
        return datetime.date(int(value[0]), int(value[1]), int(value[2]))
 | 
			
		||||
 | 
			
		||||
    def to_url(self, value):
 | 
			
		||||
        return '{:04d}/{:02d}/{:02d}/'.format(value.year, value.month,
 | 
			
		||||
                                              value.day)
 | 
			
		||||
 | 
			
		||||
@ -1,18 +1,19 @@
 | 
			
		||||
from django.core.validators import RegexValidator
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models import F
 | 
			
		||||
from django.db.models import F, Q
 | 
			
		||||
from django.db.models.functions import Concat, Substr
 | 
			
		||||
from django.utils.translation import ugettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from content_editor.models import Region, create_plugin_base
 | 
			
		||||
 | 
			
		||||
from model_utils.models import TimeStampedModel, StatusModel
 | 
			
		||||
from model_utils.managers import InheritanceManager
 | 
			
		||||
from model_utils.managers import InheritanceQuerySet
 | 
			
		||||
from model_utils import Choices
 | 
			
		||||
from filer.fields.image import FilerImageField
 | 
			
		||||
 | 
			
		||||
from aircox import models as aircox
 | 
			
		||||
from . import plugins
 | 
			
		||||
from .converters import PagePathConverter
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Site(models.Model):
 | 
			
		||||
@ -70,12 +71,31 @@ class SiteLink(plugins.Link, SitePlugin):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#-----------------------------------------------------------------------
 | 
			
		||||
class BasePage(StatusModel):
 | 
			
		||||
    """
 | 
			
		||||
    Base abstract class for views whose url path is defined by users.
 | 
			
		||||
    Page parenting is based on foreignkey to parent and page path.
 | 
			
		||||
class PageQueryset(InheritanceQuerySet):
 | 
			
		||||
    def active(self):
 | 
			
		||||
        return self.filter(Q(status=Page.STATUS.announced) |
 | 
			
		||||
                           Q(status=Page.STATUS.published))
 | 
			
		||||
 | 
			
		||||
    Inspired by Feincms3.
 | 
			
		||||
    def descendants(self, page, direct=True, inclusive=True):
 | 
			
		||||
        qs = self.filter(parent=page) if direct else \
 | 
			
		||||
             self.filter(path__startswith=page.path)
 | 
			
		||||
        if not inclusive:
 | 
			
		||||
            qs = qs.exclude(pk=page.pk)
 | 
			
		||||
        return qs
 | 
			
		||||
 | 
			
		||||
    def ancestors(self, page, inclusive=True):
 | 
			
		||||
        path, paths = page.path, []
 | 
			
		||||
        index = path.find('/')
 | 
			
		||||
        while index != -1 and index+1 < len(path):
 | 
			
		||||
            paths.append(path[0:index+1])
 | 
			
		||||
            index = path.find('/', index+1)
 | 
			
		||||
        return self.filter(path__in=paths)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Page(StatusModel):
 | 
			
		||||
    """
 | 
			
		||||
    Base class for views whose url path can be defined by users.
 | 
			
		||||
    Page parenting is based on foreignkey to parent and page path.
 | 
			
		||||
    """
 | 
			
		||||
    STATUS = Choices('draft', 'announced', 'published')
 | 
			
		||||
 | 
			
		||||
@ -89,22 +109,22 @@ class BasePage(StatusModel):
 | 
			
		||||
    path = models.CharField(
 | 
			
		||||
        _("path"), max_length=1000,
 | 
			
		||||
        blank=True, db_index=True, unique=True,
 | 
			
		||||
        validators=[
 | 
			
		||||
            RegexValidator(
 | 
			
		||||
                regex=r"^/(|.+/)$",
 | 
			
		||||
                message=_("Path must start and end with a slash (/)."),
 | 
			
		||||
            )
 | 
			
		||||
        ],
 | 
			
		||||
        validators=[RegexValidator(
 | 
			
		||||
            regex=PagePathConverter.regex,
 | 
			
		||||
            message=_('Path accepts alphanumeric and "_-" characters '
 | 
			
		||||
                      'and must be surrounded by "/"')
 | 
			
		||||
        )],
 | 
			
		||||
    )
 | 
			
		||||
    static_path = models.BooleanField(
 | 
			
		||||
        _('static path'), default=False,
 | 
			
		||||
        # FIXME: help
 | 
			
		||||
        help_text=_('Update path using parent\'s page path and page title')
 | 
			
		||||
    )
 | 
			
		||||
    headline = models.TextField(
 | 
			
		||||
        _('headline'), max_length=128, blank=True, null=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    objects = InheritanceManager()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        abstract = True
 | 
			
		||||
    objects = PageQueryset.as_manager()
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
@ -112,10 +132,14 @@ class BasePage(StatusModel):
 | 
			
		||||
        self._initial_parent = self.parent
 | 
			
		||||
        self._initial_slug = self.slug
 | 
			
		||||
 | 
			
		||||
    def view(self, request, *args, **kwargs):
 | 
			
		||||
    def get_view_class(self):
 | 
			
		||||
        """ Page view class"""
 | 
			
		||||
        raise NotImplementedError('not implemented')
 | 
			
		||||
 | 
			
		||||
    def view(self, request, *args, site=None, **kwargs):
 | 
			
		||||
        """ Page view function """
 | 
			
		||||
        from django.http import HttpResponse
 | 
			
		||||
        return HttpResponse('Not implemented')
 | 
			
		||||
        view = self.get_view_class().as_view(site=site, page=self)
 | 
			
		||||
        return view(request, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def update_descendants(self):
 | 
			
		||||
        """ Update descendants pages' path if required. """
 | 
			
		||||
@ -123,8 +147,10 @@ class BasePage(StatusModel):
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        # FIXME: draft -> draft children?
 | 
			
		||||
        expr = Concat(self.path, Substr(F('path'), len(self._initial_path)))
 | 
			
		||||
        BasePage.objects.filter(path__startswith=self._initial_path) \
 | 
			
		||||
        # FIXME: Page.objects (can't use Page since its an abstract model)
 | 
			
		||||
        if len(self._initial_path):
 | 
			
		||||
            expr = Concat('path', Substr(F('path'), len(self._initial_path)))
 | 
			
		||||
            Page.objects.filter(path__startswith=self._initial_path) \
 | 
			
		||||
                        .update(path=expr)
 | 
			
		||||
 | 
			
		||||
    def sync_generations(self, update_descendants=True):
 | 
			
		||||
@ -141,13 +167,13 @@ class BasePage(StatusModel):
 | 
			
		||||
 | 
			
		||||
        if not self.title or not self.path or self.static_path and \
 | 
			
		||||
                self.slug != self._initial_slug:
 | 
			
		||||
            self.path = self.parent.path + '/' + self.slug \
 | 
			
		||||
            self.path = self.parent.path + self.slug \
 | 
			
		||||
                if self.parent is not None else '/' + self.slug
 | 
			
		||||
 | 
			
		||||
        if self.path[-1] != '/':
 | 
			
		||||
            self.path += '/'
 | 
			
		||||
        if self.path[0] != '/':
 | 
			
		||||
            self.path = '/' + self.path
 | 
			
		||||
        if self.path[-1] != '/':
 | 
			
		||||
            self.path += '/'
 | 
			
		||||
        if update_descendants:
 | 
			
		||||
            self.update_descendants()
 | 
			
		||||
 | 
			
		||||
@ -155,18 +181,23 @@ class BasePage(StatusModel):
 | 
			
		||||
        self.sync_generations(update_descendants)
 | 
			
		||||
        super().save(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return '{}: {}'.format(self._meta.verbose_name,
 | 
			
		||||
                               self.title or self.pk)
 | 
			
		||||
 | 
			
		||||
class Page(BasePage, TimeStampedModel):
 | 
			
		||||
 | 
			
		||||
class Article(Page, TimeStampedModel):
 | 
			
		||||
    """ User's pages """
 | 
			
		||||
    regions = [
 | 
			
		||||
        Region(key="main", title=_("Content")),
 | 
			
		||||
        Region(key="content", title=_("Content")),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    # metadata
 | 
			
		||||
    as_program = models.ForeignKey(
 | 
			
		||||
        aircox.Program, models.SET_NULL, blank=True, null=True,
 | 
			
		||||
        related_name='published_pages',
 | 
			
		||||
        limit_choices_to={'schedule__isnull': False},
 | 
			
		||||
        # SO#51948640
 | 
			
		||||
        # limit_choices_to={'schedule__isnull': False},
 | 
			
		||||
        verbose_name=_('Show program as author'),
 | 
			
		||||
        help_text=_("Show program as author"),
 | 
			
		||||
    )
 | 
			
		||||
@ -180,45 +211,41 @@ class Page(BasePage, TimeStampedModel):
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # content
 | 
			
		||||
    headline = models.TextField(
 | 
			
		||||
        _('headline'), max_length=128, blank=True, null=True,
 | 
			
		||||
    )
 | 
			
		||||
    cover = FilerImageField(
 | 
			
		||||
        on_delete=models.SET_NULL, null=True, blank=True,
 | 
			
		||||
        verbose_name=_('Cover'),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def get_view_class(self):
 | 
			
		||||
        from .views import PageView
 | 
			
		||||
        return PageView
 | 
			
		||||
 | 
			
		||||
    def view(self, request, *args, **kwargs):
 | 
			
		||||
        """ Page view function """
 | 
			
		||||
        view = self.get_view_class().as_view()
 | 
			
		||||
        return view(request, *args, **kwargs)
 | 
			
		||||
        from .views import ArticleView
 | 
			
		||||
        return ArticleView
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DiffusionPage(Page):
 | 
			
		||||
class DiffusionPage(Article):
 | 
			
		||||
    diffusion = models.OneToOneField(
 | 
			
		||||
        aircox.Diffusion, models.CASCADE,
 | 
			
		||||
        blank=True, null=True,
 | 
			
		||||
        related_name='page',
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ProgramPage(Page):
 | 
			
		||||
class ProgramPage(Article):
 | 
			
		||||
    program = models.OneToOneField(
 | 
			
		||||
        aircox.Program, models.CASCADE,
 | 
			
		||||
        blank=True, null=True,
 | 
			
		||||
        related_name='page',
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def get_view_class(self):
 | 
			
		||||
        from .views import ProgramView
 | 
			
		||||
        return ProgramView
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#-----------------------------------------------------------------------
 | 
			
		||||
PagePlugin = create_plugin_base(Page)
 | 
			
		||||
ArticlePlugin = create_plugin_base(Article)
 | 
			
		||||
 | 
			
		||||
class PageRichText(plugins.RichText, PagePlugin):
 | 
			
		||||
class ArticleRichText(plugins.RichText, ArticlePlugin):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
class PageImage(plugins.Image, PagePlugin):
 | 
			
		||||
class ArticleImage(plugins.Image, ArticlePlugin):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -18,6 +18,7 @@
 | 
			
		||||
    "ttf-loader": "^1.0.2",
 | 
			
		||||
    "vue-loader": "^15.7.0",
 | 
			
		||||
    "vue-style-loader": "^4.1.2",
 | 
			
		||||
    "vue-template-compiler": "^2.6.10",
 | 
			
		||||
    "webpack": "^4.32.2",
 | 
			
		||||
    "webpack-cli": "^3.3.2"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,6 @@ site_renderer.register(SiteLink, lambda plugin: plugin.render())
 | 
			
		||||
 | 
			
		||||
page_renderer = PluginRenderer()
 | 
			
		||||
page_renderer._renderers.clear()
 | 
			
		||||
page_renderer.register(PageRichText, lambda plugin: mark_safe(plugin.text))
 | 
			
		||||
page_renderer.register(PageImage, lambda plugin: plugin.render())
 | 
			
		||||
page_renderer.register(ArticleRichText, lambda plugin: mark_safe(plugin.text))
 | 
			
		||||
page_renderer.register(ArticleImage, lambda plugin: plugin.render())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										17
									
								
								aircox_web/templates/aircox_web/article.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								aircox_web/templates/aircox_web/article.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
			
		||||
{% extends "aircox_web/page.html" %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{% block main %}
 | 
			
		||||
<section class="is-inline-block">
 | 
			
		||||
    <img class="cover" src="{{ page.cover.url }}"/>
 | 
			
		||||
    {% block headline %}
 | 
			
		||||
    {{ page.headline }}
 | 
			
		||||
    {% endblock %}
 | 
			
		||||
 | 
			
		||||
    {% block content %}
 | 
			
		||||
    {{ regions.main }}
 | 
			
		||||
    {% endblock %}
 | 
			
		||||
</section>
 | 
			
		||||
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
@ -1,50 +0,0 @@
 | 
			
		||||
{% load static i18n thumbnail %}
 | 
			
		||||
<html>
 | 
			
		||||
    <head>
 | 
			
		||||
        <meta charset="utf-8">
 | 
			
		||||
        <meta name="application-name" content="aircox">
 | 
			
		||||
        <meta name="description" content="{{ site.description }}">
 | 
			
		||||
        <meta name="keywords" content="{{ site.tags }}">
 | 
			
		||||
        <link rel="icon" href="{% thumbnail site.favicon 32x32 crop %}" />
 | 
			
		||||
 | 
			
		||||
        {% block assets %}
 | 
			
		||||
        <link rel="stylesheet" type="text/css" href="{% static "aircox_web/assets/main.css" %}"/>
 | 
			
		||||
        <!-- <link rel="stylesheet" type="text/css" href="{% static "aircox_web/assets/vendor.css" %}"/> -->
 | 
			
		||||
 | 
			
		||||
        <script src="{% static "aircox_web/assets/main.js" %}"></script>
 | 
			
		||||
        <script src="{% static "aircox_web/assets/vendor.js" %}"></script>
 | 
			
		||||
        {% endblock %}
 | 
			
		||||
 | 
			
		||||
        <title>{% block title %}{{ site.title }}{% endblock %}</title>
 | 
			
		||||
 | 
			
		||||
        {% block extra_head %}{% endblock %}
 | 
			
		||||
    </head>
 | 
			
		||||
    <body id="app">
 | 
			
		||||
        <nav class="navbar has-shadow" role="navigation" aria-label="main navigation">
 | 
			
		||||
            <div class="navbar-brand">
 | 
			
		||||
                <a href="/" title="{% trans "Home" %}" class="navbar-item">
 | 
			
		||||
                    <img src="{{ site.logo.url }}" class="logo"/>
 | 
			
		||||
                </a>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="navbar-menu">
 | 
			
		||||
                <div class="navbar-start">
 | 
			
		||||
                    {{ site_regions.topnav }}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </nav>
 | 
			
		||||
 | 
			
		||||
        <div class="columns">
 | 
			
		||||
            <aside class="column">
 | 
			
		||||
                {{ site_regions.sidenav }}
 | 
			
		||||
            </aside>
 | 
			
		||||
            <main class="column is-three-quarters">
 | 
			
		||||
                {% block main %}
 | 
			
		||||
                <h1>{{ page.title }}</h1>
 | 
			
		||||
                {{ regions.main }}
 | 
			
		||||
                {% endblock main %}
 | 
			
		||||
            </main>
 | 
			
		||||
        </div>
 | 
			
		||||
    </body>
 | 
			
		||||
</html>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										43
									
								
								aircox_web/templates/aircox_web/diffusion_item.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								aircox_web/templates/aircox_web/diffusion_item.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,43 @@
 | 
			
		||||
{% load i18n easy_thumbnails_tags aircox_web %}
 | 
			
		||||
{% comment %}
 | 
			
		||||
Context variables:
 | 
			
		||||
- object: the actual diffusion
 | 
			
		||||
- page: current parent page in which item is rendered
 | 
			
		||||
{% endcomment %}
 | 
			
		||||
 | 
			
		||||
{% with page as context_page %}
 | 
			
		||||
{% with object.program as program %}
 | 
			
		||||
{% diffusion_page object as page %}
 | 
			
		||||
<article class="media">
 | 
			
		||||
    <div class="media-left">
 | 
			
		||||
      <figure class="image is-64x64">
 | 
			
		||||
          <img src="{% thumbnail page.cover|default:site.logo 128x128 crop=scale %}">
 | 
			
		||||
      </figure>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="media-content">
 | 
			
		||||
      <div class="content">
 | 
			
		||||
        <p>
 | 
			
		||||
          {% if page and context_page != page %}
 | 
			
		||||
          <strong><a href="{{ page.path }}">{{ page.title }}</a></strong>
 | 
			
		||||
          {% else %}
 | 
			
		||||
          <strong>{{ page.title|default:program.name }}</strong>
 | 
			
		||||
          {% endif %}
 | 
			
		||||
          {% if object.page is page %}
 | 
			
		||||
          — <a href="{{ program.page.path }}">{{ program.name }}</a></small>
 | 
			
		||||
          {% 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 %}
 | 
			
		||||
          <br>
 | 
			
		||||
          {{ page.headline|default:program.page.headline }}
 | 
			
		||||
        </p>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</article>
 | 
			
		||||
{% endwith %}
 | 
			
		||||
{% endwith %}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										46
									
								
								aircox_web/templates/aircox_web/diffusions.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								aircox_web/templates/aircox_web/diffusions.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,46 @@
 | 
			
		||||
{% extends "aircox_web/page.html" %}
 | 
			
		||||
{% load i18n aircox_web %}
 | 
			
		||||
 | 
			
		||||
{% block main %}
 | 
			
		||||
{{ block.super }}
 | 
			
		||||
 | 
			
		||||
<section class="section">
 | 
			
		||||
    {% for object in object_list %}
 | 
			
		||||
    <div class="columns">
 | 
			
		||||
        <div class="column is-one-fifth has-text-right">
 | 
			
		||||
            <time datetime="{{ object.start|date:"c" }}" title="{{ object.start }}">
 | 
			
		||||
                {{ object.start|date:"d M, H:i" }}
 | 
			
		||||
            </time>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="column">
 | 
			
		||||
            {% include "aircox_web/diffusion_item.html" %}
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{% 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">
 | 
			
		||||
        {% trans "Previous" %}</a>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
    {% if page_obj.has_next %}
 | 
			
		||||
    <a href="?page={{ page_obj.next_page_number }}" class="pagination-next">
 | 
			
		||||
        {% trans "Next" %}</a>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
    <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 %}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										19
									
								
								aircox_web/templates/aircox_web/log_item.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								aircox_web/templates/aircox_web/log_item.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
 | 
			
		||||
{% with object.track as track %}
 | 
			
		||||
<span class="has-text-info is-size-5">♬</span>
 | 
			
		||||
<span>{{ track.title }}</span>
 | 
			
		||||
{% with track.artist as artist %}
 | 
			
		||||
{% with track.info as info %}
 | 
			
		||||
<span class="has-text-grey-dark has-text-weight-light">
 | 
			
		||||
    {% blocktrans %}
 | 
			
		||||
    by {{ artist }}
 | 
			
		||||
    {% endblocktrans %}
 | 
			
		||||
    {% if info %}
 | 
			
		||||
    ({% blocktrans %}<i>{{ info }}</i>{% endblocktrans %})
 | 
			
		||||
    {% endif %}
 | 
			
		||||
</span>
 | 
			
		||||
{% endwith %}
 | 
			
		||||
{% endwith %}
 | 
			
		||||
{% endwith %}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										51
									
								
								aircox_web/templates/aircox_web/logs.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								aircox_web/templates/aircox_web/logs.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,51 @@
 | 
			
		||||
{% extends "aircox_web/page.html" %}
 | 
			
		||||
{% load i18n aircox_web %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{% block main %}
 | 
			
		||||
{{ block.super }}
 | 
			
		||||
 | 
			
		||||
<section class="section">
 | 
			
		||||
    {% if dates %}
 | 
			
		||||
    <nav class="tabs is-centered" aria-label="{% trans "Other days' logs" %}">
 | 
			
		||||
        <ul>
 | 
			
		||||
        {% for day in dates %}
 | 
			
		||||
            <li {% if day == date %}class="is-active"{% endif %}>
 | 
			
		||||
                <a href="{% url "logs" date=day %}">
 | 
			
		||||
                    {{ day|date:"d b" }}
 | 
			
		||||
                </a>
 | 
			
		||||
            </li>
 | 
			
		||||
 | 
			
		||||
        {% if forloop.last and day > min_date %}
 | 
			
		||||
            <li>...</li>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
        {% endfor %}
 | 
			
		||||
        </ul>
 | 
			
		||||
    </nav>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
    {# <h4 class="subtitle size-4">{{ date }}</h4> #}
 | 
			
		||||
    <table class="table is-striped is-hoverable is-fullwidth">
 | 
			
		||||
        {% for object in object_list reversed %}
 | 
			
		||||
        <tr>
 | 
			
		||||
        {% if object|is_diffusion %}
 | 
			
		||||
            <td>
 | 
			
		||||
                <time datetime="{{ object.start }}" title="{{ object.start }}">
 | 
			
		||||
                    {{ object.start|date:"H:i" }} - {{ object.end|date:"H:i" }}
 | 
			
		||||
                </time>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td>{% include "aircox_web/diffusion_item.html" %}</td>
 | 
			
		||||
        {% else %}
 | 
			
		||||
            <td>
 | 
			
		||||
                <time datetime="{{ object.date }}" title="{{ object.date }}">
 | 
			
		||||
                    {{ object.date|date:"H:i" }}
 | 
			
		||||
                </time>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td>{% include "aircox_web/log_item.html" %}</td>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
        </tr>
 | 
			
		||||
        {% endfor %}
 | 
			
		||||
    </table>
 | 
			
		||||
</section>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,59 @@
 | 
			
		||||
{% extends "aircox_web/base.html" %}
 | 
			
		||||
{% load static i18n thumbnail %}
 | 
			
		||||
<html>
 | 
			
		||||
    <head>
 | 
			
		||||
        <meta charset="utf-8">
 | 
			
		||||
        <meta name="application-name" content="aircox">
 | 
			
		||||
        <meta name="description" content="{{ site.description }}">
 | 
			
		||||
        <meta name="keywords" content="{{ site.tags }}">
 | 
			
		||||
        <link rel="icon" href="{% thumbnail site.favicon 32x32 crop %}" />
 | 
			
		||||
 | 
			
		||||
{% block title %}{{ page.title }} -- {{ block.super }}{% endblock %}
 | 
			
		||||
        {% block assets %}
 | 
			
		||||
        <link rel="stylesheet" type="text/css" href="{% static "aircox_web/assets/main.css" %}"/>
 | 
			
		||||
        <script src="{% static "aircox_web/assets/main.js" %}"></script>
 | 
			
		||||
        <script src="{% static "aircox_web/assets/vendor.js" %}"></script>
 | 
			
		||||
        {% endblock %}
 | 
			
		||||
 | 
			
		||||
        <title>{% block title %}{% if title %}{{ title }} -- {% endif %}{{ site.title }}{% endblock %}</title>
 | 
			
		||||
 | 
			
		||||
        {% block extra_head %}{% endblock %}
 | 
			
		||||
    </head>
 | 
			
		||||
    <body>
 | 
			
		||||
        <div id="app">
 | 
			
		||||
            <nav class="navbar has-shadow" role="navigation" aria-label="main navigation">
 | 
			
		||||
                <div class="container">
 | 
			
		||||
                    <div class="navbar-brand">
 | 
			
		||||
                        <a href="/" title="{% trans "Home" %}" class="navbar-item">
 | 
			
		||||
                            <img src="{{ site.logo.url }}" class="logo"/>
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="navbar-menu">
 | 
			
		||||
                        <div class="navbar-start">
 | 
			
		||||
                            {{ site_regions.topnav }}
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </nav>
 | 
			
		||||
 | 
			
		||||
            <div class="container">
 | 
			
		||||
                <div class="columns">
 | 
			
		||||
                    <aside class="column is-one-quarter">
 | 
			
		||||
                        {% block left-sidebar %}
 | 
			
		||||
                        {{ site_regions.sidenav }}
 | 
			
		||||
                        {% endblock %}
 | 
			
		||||
                    </aside>
 | 
			
		||||
                    <main class="column page">
 | 
			
		||||
                        <header class="header">
 | 
			
		||||
                            {% block header %}
 | 
			
		||||
                            <h1 class="title is-1">{{ title }}</h1>
 | 
			
		||||
                            {% endblock %}
 | 
			
		||||
                        </header>
 | 
			
		||||
 | 
			
		||||
                        {% block main %}{% endblock main %}
 | 
			
		||||
                    </main>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </body>
 | 
			
		||||
</html>
 | 
			
		||||
 | 
			
		||||
{% block main %}
 | 
			
		||||
<h1 class="title">{{ page.title }}</h1>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										27
									
								
								aircox_web/templates/aircox_web/program.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								aircox_web/templates/aircox_web/program.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
			
		||||
{% extends "aircox_web/article.html" %}
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
 | 
			
		||||
{% block headline %}
 | 
			
		||||
<section class="is-size-5">
 | 
			
		||||
    {% for schedule in program.schedule_set.all %}
 | 
			
		||||
    <p>
 | 
			
		||||
        <strong>{{ schedule.datetime|date:"l H:i" }}</strong>
 | 
			
		||||
        <small>
 | 
			
		||||
            {{ schedule.get_frequency_display }}
 | 
			
		||||
            {% if schedule.initial %}
 | 
			
		||||
            {% with schedule.initial.date as date %}
 | 
			
		||||
            <span title="{% blocktrans %}Rerun of {{ date }}{% endblocktrans %}">
 | 
			
		||||
                / {% trans "rerun" %}
 | 
			
		||||
            </span>
 | 
			
		||||
            {% endwith %}
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </small>
 | 
			
		||||
    </p>
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
</section>
 | 
			
		||||
 | 
			
		||||
{{ block.super }}
 | 
			
		||||
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										52
									
								
								aircox_web/templates/aircox_web/timetable.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								aircox_web/templates/aircox_web/timetable.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,52 @@
 | 
			
		||||
{% extends "aircox_web/page.html" %}
 | 
			
		||||
{% load i18n aircox_web %}
 | 
			
		||||
 | 
			
		||||
{% block main %}
 | 
			
		||||
{{ block.super }}
 | 
			
		||||
 | 
			
		||||
<section class="section">
 | 
			
		||||
    <h3 class="subtitle size-3">
 | 
			
		||||
        {% blocktrans %}From <b>{{ start }}</b> to <b>{{ end }}</b>{% endblocktrans %}
 | 
			
		||||
    </h3>
 | 
			
		||||
 | 
			
		||||
    {% unique_id "timetable" as timetable_id %}
 | 
			
		||||
    <a-tabs default="{{ date }}">
 | 
			
		||||
        <template v-slot:tabs="scope" noscript="hidden">
 | 
			
		||||
            <li><a href="{% url "timetable" date=prev_date %}"><</a></li>
 | 
			
		||||
 | 
			
		||||
            {% for day in by_date.keys %}
 | 
			
		||||
            <a-tab value="{{ day }}">
 | 
			
		||||
                <a href="{% url "timetable" date=day %}">
 | 
			
		||||
                    {{ day|date:"D. d" }}
 | 
			
		||||
                </a>
 | 
			
		||||
            </a-tab>
 | 
			
		||||
            {% endfor %}
 | 
			
		||||
 | 
			
		||||
            <li>
 | 
			
		||||
                <a href="{% url "timetable" date=next_date %}">></a>
 | 
			
		||||
            </li>
 | 
			
		||||
        </template>
 | 
			
		||||
 | 
			
		||||
        <template v-slot:default="{value}">
 | 
			
		||||
            {% for day, diffusions in by_date.items %}
 | 
			
		||||
            <noscript><h4 class="subtitle is-4">{{ day|date:"l d F Y" }}</h4></noscript>
 | 
			
		||||
            <div id="{{timetable_id}}-{{ day|date:"Y-m-d" }}" v-if="value == '{{ day }}'">
 | 
			
		||||
                {% for object in diffusions %}
 | 
			
		||||
                <div class="columns">
 | 
			
		||||
                    <div class="column is-one-fifth has-text-right">
 | 
			
		||||
                        <time datetime="{{ object.start|date:"c" }}">
 | 
			
		||||
                            {{ object.start|date:"H:i" }} - {{ object.end|date:"H:i" }}
 | 
			
		||||
                        </time>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="column">
 | 
			
		||||
                        {% include "aircox_web/diffusion_item.html" %}
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            </div>
 | 
			
		||||
            {% endfor %}
 | 
			
		||||
        </template>
 | 
			
		||||
    </a-tabs>
 | 
			
		||||
</section>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										28
									
								
								aircox_web/templatetags/aircox_web.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								aircox_web/templatetags/aircox_web.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
			
		||||
import random
 | 
			
		||||
 | 
			
		||||
from django import template
 | 
			
		||||
 | 
			
		||||
from aircox import models as aircox
 | 
			
		||||
from aircox_web.models import Page
 | 
			
		||||
 | 
			
		||||
random.seed()
 | 
			
		||||
register = template.Library()
 | 
			
		||||
 | 
			
		||||
@register.simple_tag(name='diffusion_page')
 | 
			
		||||
def do_diffusion_page(diffusion):
 | 
			
		||||
    """ Return page for diffusion. """
 | 
			
		||||
    for obj in (diffusion, diffusion.program):
 | 
			
		||||
        page = getattr(obj, 'page', None)
 | 
			
		||||
        if page is not None and page.status is not Page.STATUS.draft:
 | 
			
		||||
            return page
 | 
			
		||||
 | 
			
		||||
@register.simple_tag(name='unique_id')
 | 
			
		||||
def do_unique_id(prefix=''):
 | 
			
		||||
    value = str(random.random()).replace('.', '')
 | 
			
		||||
    return prefix + '_' + value if prefix else value
 | 
			
		||||
 | 
			
		||||
@register.filter(name='is_diffusion')
 | 
			
		||||
def do_is_diffusion(obj):
 | 
			
		||||
    return isinstance(obj, aircox.Diffusion)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,23 @@
 | 
			
		||||
from django.conf.urls import url
 | 
			
		||||
from django.urls import path, register_converter
 | 
			
		||||
 | 
			
		||||
from . import views
 | 
			
		||||
from . import views, models
 | 
			
		||||
from .converters import PagePathConverter, DateConverter
 | 
			
		||||
 | 
			
		||||
register_converter(PagePathConverter, 'page_path')
 | 
			
		||||
register_converter(DateConverter, 'date')
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    url(r"^(?P<path>[-\w/]+)/$", views.route_page, name="page"),
 | 
			
		||||
    url(r"^$", views.route_page, name="root"),
 | 
			
		||||
    path('diffusions/',
 | 
			
		||||
         views.TimetableView.as_view(), name='timetable'),
 | 
			
		||||
    path('diffusions/<date:date>',
 | 
			
		||||
         views.TimetableView.as_view(), name='timetable'),
 | 
			
		||||
    path('diffusions/all',
 | 
			
		||||
         views.DiffusionsView.as_view(), name='diffusion-list'),
 | 
			
		||||
    path('diffusions/<slug:program>',
 | 
			
		||||
         views.DiffusionsView.as_view(), name='diffusion-list'),
 | 
			
		||||
    path('logs/', views.LogsView.as_view(), name='logs'),
 | 
			
		||||
    path('logs/<date:date>', views.LogsView.as_view(), name='logs'),
 | 
			
		||||
    path('<page_path:path>', views.route_page, name='page'),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,48 +1,241 @@
 | 
			
		||||
from django.db.models import Q
 | 
			
		||||
from django.shortcuts import get_object_or_404, render
 | 
			
		||||
from django.views.generic.base import TemplateView
 | 
			
		||||
from collections import OrderedDict, deque
 | 
			
		||||
import datetime
 | 
			
		||||
 | 
			
		||||
from django.core.paginator import Paginator
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from django.utils.translation import ugettext_lazy as _
 | 
			
		||||
from django.views.generic import TemplateView, ListView
 | 
			
		||||
from django.views.generic.base import TemplateResponseMixin, ContextMixin
 | 
			
		||||
 | 
			
		||||
from content_editor.contents import contents_for_item
 | 
			
		||||
 | 
			
		||||
from aircox import models as aircox
 | 
			
		||||
from .models import Site, Page
 | 
			
		||||
from .renderer import site_renderer, page_renderer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def route_page(request, path=None, *args, site=None, **kwargs):
 | 
			
		||||
def route_page(request, path=None, *args, model=None, site=None, **kwargs):
 | 
			
		||||
    """
 | 
			
		||||
    Route request to page of the provided path. If model is provided, uses
 | 
			
		||||
    it.
 | 
			
		||||
    """
 | 
			
		||||
    # TODO/FIXME: django site framework | site from request host
 | 
			
		||||
    # TODO: extra page kwargs (as in pepr)
 | 
			
		||||
    site = Site.objects.all().order_by('-default').first() \
 | 
			
		||||
           if site is None else site
 | 
			
		||||
 | 
			
		||||
    model = model if model is not None else Page
 | 
			
		||||
    page = get_object_or_404(
 | 
			
		||||
        # TODO: published
 | 
			
		||||
        Page.objects.select_subclasses()
 | 
			
		||||
                    .filter(Q(status=Page.STATUS.published) |
 | 
			
		||||
                            Q(status=Page.STATUS.announced)),
 | 
			
		||||
        path="/{}/".format(path) if path else "/",
 | 
			
		||||
        model.objects.select_subclasses().active(),
 | 
			
		||||
        path=path
 | 
			
		||||
    )
 | 
			
		||||
    kwargs['page'] = page
 | 
			
		||||
    return page.view(request, *args, site=site, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PageView(TemplateView):
 | 
			
		||||
    """ Base view class for pages. """
 | 
			
		||||
    template_name = 'aircox_web/page.html'
 | 
			
		||||
 | 
			
		||||
class BaseView(TemplateResponseMixin, ContextMixin):
 | 
			
		||||
    title = None
 | 
			
		||||
    site = None
 | 
			
		||||
 | 
			
		||||
    def dispatch(self, request, *args, site=None, **kwargs):
 | 
			
		||||
        self.site = site if site is not None else \
 | 
			
		||||
            Site.objects.all().order_by('-default').first()
 | 
			
		||||
        return super().dispatch(request, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        if kwargs.get('site_regions') is None:
 | 
			
		||||
            contents = contents_for_item(self.site, site_renderer._renderers.keys())
 | 
			
		||||
            kwargs['site_regions'] = contents.render_regions(site_renderer)
 | 
			
		||||
 | 
			
		||||
        kwargs.setdefault('site', self.site)
 | 
			
		||||
        if self.title is not None:
 | 
			
		||||
            kwargs.setdefault('title', self.title)
 | 
			
		||||
        return super().get_context_data(**kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ArticleView(BaseView, TemplateView):
 | 
			
		||||
    """ Base view class for pages. """
 | 
			
		||||
    template_name = 'aircox_web/article.html'
 | 
			
		||||
    page = None
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        page = kwargs.setdefault('page', self.page or self.kwargs.get('site'))
 | 
			
		||||
        site = kwargs.setdefault('site', self.site or self.kwargs.get('site'))
 | 
			
		||||
 | 
			
		||||
        # article content
 | 
			
		||||
        page = kwargs.setdefault('page', self.page or self.kwargs.get('page'))
 | 
			
		||||
        if kwargs.get('regions') is None:
 | 
			
		||||
            contents = contents_for_item(page, page_renderer._renderers.keys())
 | 
			
		||||
            kwargs['regions'] = contents.render_regions(page_renderer)
 | 
			
		||||
 | 
			
		||||
        if kwargs.get('site_regions') is None:
 | 
			
		||||
            contents = contents_for_item(site, site_renderer._renderers.keys())
 | 
			
		||||
            kwargs['site_regions'] = contents.render_regions(site_renderer)
 | 
			
		||||
        kwargs.setdefault('title', page.title)
 | 
			
		||||
        return super().get_context_data(**kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ProgramView(ArticleView):
 | 
			
		||||
    """ Base view class for pages. """
 | 
			
		||||
    template_name = 'aircox_web/program.html'
 | 
			
		||||
    next_diffs_count = 5
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, program=None, **kwargs):
 | 
			
		||||
        # TODO: pagination
 | 
			
		||||
        program = program or self.page.program
 | 
			
		||||
        #next_diffs = program.diffusion_set.on_air().after().order_by('start')
 | 
			
		||||
        return super().get_context_data(
 | 
			
		||||
            program=program,
 | 
			
		||||
            # next_diffs=next_diffs[:self.next_diffs_count],
 | 
			
		||||
            **kwargs,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DiffusionView(ArticleView):
 | 
			
		||||
    template_name = 'aircox_web/diffusion.html'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DiffusionsView(BaseView, ListView):
 | 
			
		||||
    template_name = 'aircox_web/diffusions.html'
 | 
			
		||||
    model = aircox.Diffusion
 | 
			
		||||
    paginate_by = 10
 | 
			
		||||
    title = _('Diffusions')
 | 
			
		||||
    program = None
 | 
			
		||||
 | 
			
		||||
    # TODO: get program object + display program title when filtered by program
 | 
			
		||||
    # TODO: pagination: in template, only a limited number of pages displayed
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        qs = super().get_queryset().station(self.site.station).on_air() \
 | 
			
		||||
                    .filter(initial__isnull=True) #TODO, page__isnull=False)
 | 
			
		||||
        program = self.kwargs.get('program')
 | 
			
		||||
        if program:
 | 
			
		||||
            qs = qs.filter(program__page__slug=program)
 | 
			
		||||
        return qs.order_by('-start')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TimetableView(BaseView, ListView):
 | 
			
		||||
    """ View for timetables """
 | 
			
		||||
    template_name = 'aircox_web/timetable.html'
 | 
			
		||||
    model = aircox.Diffusion
 | 
			
		||||
 | 
			
		||||
    title = _('Timetable')
 | 
			
		||||
 | 
			
		||||
    date = None
 | 
			
		||||
    start = None
 | 
			
		||||
    end = None
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        self.date = self.kwargs.get('date', datetime.date.today())
 | 
			
		||||
        self.start = self.date - datetime.timedelta(days=self.date.weekday())
 | 
			
		||||
        self.end = self.date + datetime.timedelta(days=7-self.date.weekday())
 | 
			
		||||
        return super().get_queryset().station(self.site.station) \
 | 
			
		||||
                                     .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 not diffusion.date 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
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LogViewBase(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 aircox.Diffusion.objects.station(self.station).on_air() \
 | 
			
		||||
                               .at(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 = diffs.popleft()
 | 
			
		||||
        last_collision = None
 | 
			
		||||
 | 
			
		||||
        # diff.start < log on first diff
 | 
			
		||||
        # diff.end > log on last diff
 | 
			
		||||
 | 
			
		||||
        for index, log in enumerate(logs):
 | 
			
		||||
            # get next diff
 | 
			
		||||
            if diff.end < log.date:
 | 
			
		||||
                diff = diffs.popleft() if len(diffs) else None
 | 
			
		||||
 | 
			
		||||
            # no more diff that can collide: return list
 | 
			
		||||
            if diff is None:
 | 
			
		||||
                return object_list + logs[index:]
 | 
			
		||||
 | 
			
		||||
            # diff colliding with log
 | 
			
		||||
            if diff.start <= log.date <= diff.end:
 | 
			
		||||
                if object_list[-1] is not diff:
 | 
			
		||||
                    object_list.append(diff)
 | 
			
		||||
                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 LogsView(BaseView, LogViewBase):
 | 
			
		||||
    """ View for timetables """
 | 
			
		||||
    template_name = 'aircox_web/logs.html'
 | 
			
		||||
    model = aircox.Log
 | 
			
		||||
    title = _('Logs')
 | 
			
		||||
 | 
			
		||||
    date = None
 | 
			
		||||
    max_age = 10
 | 
			
		||||
 | 
			
		||||
    min_date = None
 | 
			
		||||
 | 
			
		||||
    def get(self, request, *args, **kwargs):
 | 
			
		||||
        self.station = self.site.station
 | 
			
		||||
 | 
			
		||||
        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
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@ const webpack = require('webpack');
 | 
			
		||||
 | 
			
		||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
 | 
			
		||||
// const { createLodashAliases } = require('lodash-loader');
 | 
			
		||||
const { VueLoaderPlugin } = require('vue-loader');
 | 
			
		||||
const VueLoaderPlugin = require('vue-loader/lib/plugin');
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
module.exports = (env, argv) => Object({
 | 
			
		||||
@ -29,6 +29,13 @@ module.exports = (env, argv) => Object({
 | 
			
		||||
 | 
			
		||||
                    test: /[\\/]node_modules[\\/]/,
 | 
			
		||||
                },
 | 
			
		||||
 | 
			
		||||
                /*noscript: {
 | 
			
		||||
                    name: 'noscript',
 | 
			
		||||
                    chunks: 'initial',
 | 
			
		||||
                    enforce: true,
 | 
			
		||||
                    test: /noscript/,
 | 
			
		||||
                }*/
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
@ -43,6 +50,7 @@ module.exports = (env, argv) => Object({
 | 
			
		||||
 | 
			
		||||
    module: {
 | 
			
		||||
        rules: [
 | 
			
		||||
            { test: /\.vue$/, loader: 'vue-loader' },
 | 
			
		||||
            {
 | 
			
		||||
                test: /\/node_modules\//,
 | 
			
		||||
                sideEffects: false
 | 
			
		||||
@ -64,7 +72,6 @@ module.exports = (env, argv) => Object({
 | 
			
		||||
                    }
 | 
			
		||||
                }],
 | 
			
		||||
            },
 | 
			
		||||
            { test: /\.vue$/, use: 'vue-loader' },
 | 
			
		||||
        ],
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user