work on website + page becomes concrete

This commit is contained in:
bkfox 2019-09-05 14:12:12 +02:00
parent 595af5a69d
commit c46f006379
88 changed files with 476 additions and 9823 deletions

View File

@ -1,24 +1,10 @@
# Aircox Programs # Aircox
Aircox application aims to provide basis of a radio management system.
This application defines all base models and basic control of them. We have:
* **Nameable**: generic class used in any class needing to be named. Includes some utility functions;
* **Station**: a station
* **Program**: the program itself;
* **Diffusion**: occurrence of a program planified in the timetable. For rerun, informations are bound to the initial diffusion;
* **Schedule**: describes diffusions frequencies for each program;
* **Track**: track informations in a playlist of a diffusion;
* **Sound**: information about a sound that can be used for podcast or rerun;
* **Log**: logs
## Architecture ## Architecture
A Station is basically an object that represent a radio station. On each station, we use the Program object, that is declined in two different types: A Station contains programs that can be scheduled or streamed. A *Scheduled Program* is a regular show that has planified diffusions of its occurences (episodes). A *Streamed Program* is a program used to play randoms musics between the shows.
* **Scheduled**: the diffusion is based on a timetable and planified through one Schedule or more; Diffusion object represent the occurrence of these programs;
* **Streamed**: the diffusion is based on random playlist, used to fill gaps between the programs;
Each program has a directory in **AIRCOX_PROGRAMS_DIR**; For each, subdir: Each program has a directory on the server where user puts its podcasts (in **AIRCOX_PROGRAM_DIR**). It contains the directories **archives** (complete show's podcasts) and **excerpts** (partial or whatever podcasts).
* **archives**: complete episode record, can be used for diffusions or as a podcast
* **excerpts**: excerpt of an episode, or other elements, can be used as a podcast
## manage.py's commands ## manage.py's commands

View File

@ -25,6 +25,8 @@ class PageAdmin(admin.ModelAdmin):
list_editable = ('status', 'category') list_editable = ('status', 'category')
prepopulated_fields = {"slug": ("title",)} prepopulated_fields = {"slug": ("title",)}
change_form_template = 'admin/aircox/page_change_form.html'
fieldsets = [ fieldsets = [
('', { ('', {
'fields': ['title', 'slug', 'category', 'cover', 'content'], 'fields': ['title', 'slug', 'category', 'cover', 'content'],

View File

@ -78,18 +78,14 @@ class Streamer:
@property @property
def inputs(self): def inputs(self):
""" Return input ports of the station """ """ Return input ports of the station """
return self.station.port_set.filter( return self.station.port_set.filter(direction=Port.DIRECTION_INPUT,
direction=Port.Direction.input, active=True)
active=True
)
@property @property
def outputs(self): def outputs(self):
""" Return output ports of the station """ """ Return output ports of the station """
return self.station.port_set.filter( return self.station.port_set.filter(direction=Port.DIRECTION_OUTPUT,
direction=Port.Direction.output, active=True)
active=True,
)
# Sources and config ############################################### # Sources and config ###############################################
def send(self, *args, **kwargs): def send(self, *args, **kwargs):

View File

@ -57,14 +57,14 @@ class Actions:
diffusion.save() diffusion.save()
def clean(self): def clean(self):
qs = Diffusion.objects.filter(type=Diffusion.Type.unconfirmed, qs = Diffusion.objects.filter(type=Diffusion.TYPE_UNCONFIRMED,
start__lt=self.date) start__lt=self.date)
logger.info('[clean] %d diffusions will be removed', qs.count()) logger.info('[clean] %d diffusions will be removed', qs.count())
qs.delete() qs.delete()
def check(self): def check(self):
# TODO: redo # TODO: redo
qs = Diffusion.objects.filter(type=Diffusion.Type.unconfirmed, qs = Diffusion.objects.filter(type=Diffusion.TYPE_UNCONFIRMED,
start__gt=self.date) start__gt=self.date)
items = [] items = []
for diffusion in qs: for diffusion in qs:

View File

@ -184,9 +184,9 @@ class MonitorHandler(PatternMatchingEventHandler):
""" """
self.subdir = subdir self.subdir = subdir
if self.subdir == settings.AIRCOX_SOUND_ARCHIVES_SUBDIR: if self.subdir == settings.AIRCOX_SOUND_ARCHIVES_SUBDIR:
self.sound_kwargs = {'type': Sound.Type.archive} self.sound_kwargs = {'type': Sound.TYPE_ARCHIVE}
else: else:
self.sound_kwargs = {'type': Sound.Type.excerpt} self.sound_kwargs = {'type': Sound.TYPE_EXCERPT}
patterns = ['*/{}/*{}'.format(self.subdir, ext) patterns = ['*/{}/*{}'.format(self.subdir, ext)
for ext in settings.AIRCOX_SOUND_FILE_EXT] for ext in settings.AIRCOX_SOUND_FILE_EXT]
@ -213,7 +213,7 @@ class MonitorHandler(PatternMatchingEventHandler):
sound = Sound.objects.filter(path=event.src_path) sound = Sound.objects.filter(path=event.src_path)
if sound: if sound:
sound = sound[0] sound = sound[0]
sound.type = sound.Type.removed sound.type = sound.TYPE_REMOVED
sound.save() sound.save()
def on_moved(self, event): def on_moved(self, event):
@ -259,11 +259,11 @@ class Command(BaseCommand):
logger.info('#%d %s', program.id, program.title) logger.info('#%d %s', program.id, program.title)
self.scan_for_program( self.scan_for_program(
program, settings.AIRCOX_SOUND_ARCHIVES_SUBDIR, program, settings.AIRCOX_SOUND_ARCHIVES_SUBDIR,
type=Sound.Type.archive, type=Sound.TYPE_ARCHIVE,
) )
self.scan_for_program( self.scan_for_program(
program, settings.AIRCOX_SOUND_EXCERPTS_SUBDIR, program, settings.AIRCOX_SOUND_EXCERPTS_SUBDIR,
type=Sound.Type.excerpt, type=Sound.TYPE_EXCERPT,
) )
dirs.append(os.path.join(program.path)) dirs.append(os.path.join(program.path))
@ -317,7 +317,7 @@ class Command(BaseCommand):
# get available sound files # get available sound files
sounds = Sound.objects.filter(is_good_quality=False) \ sounds = Sound.objects.filter(is_good_quality=False) \
.exclude(type=Sound.Type.removed) .exclude(type=Sound.TYPE_REMOVED)
if check: if check:
self.check_sounds(sounds) self.check_sounds(sounds)

View File

@ -152,7 +152,7 @@ class Monitor:
.now(air_time).first() .now(air_time).first()
# log sound on air # log sound on air
return self.log(type=Log.Type.on_air, date=source.air_time, return self.log(type=Log.TYPE_ON_AIR, date=source.air_time,
source=source.id, sound=sound, diffusion=diff, source=source.id, sound=sound, diffusion=diff,
comment=air_uri) comment=air_uri)
@ -177,7 +177,7 @@ class Monitor:
if pos > now: if pos > now:
break break
# log track on air # log track on air
self.log(type=Log.Type.on_air, date=pos, source=log.source, self.log(type=Log.TYPE_ON_AIR, date=pos, source=log.source,
track=track, comment=track) track=track, comment=track)
def handle_diffusions(self): def handle_diffusions(self):
@ -208,7 +208,7 @@ class Monitor:
# #
now = tz.now() now = tz.now()
diff = Diffusion.objects.station(self.station).on_air().now(now) \ diff = Diffusion.objects.station(self.station).on_air().now(now) \
.filter(episode__sound__type=Sound.Type.archive) \ .filter(episode__sound__type=Sound.TYPE_ARCHIVE) \
.first() .first()
# Can't use delay: diffusion may start later than its assigned start. # Can't use delay: diffusion may start later than its assigned start.
log = None if not diff else self.logs.start().filter(diffusion=diff) log = None if not diff else self.logs.start().filter(diffusion=diff)
@ -228,13 +228,13 @@ class Monitor:
def start_diff(self, source, diff): def start_diff(self, source, diff):
playlist = Sound.objects.episode(id=diff.episode_id).paths() playlist = Sound.objects.episode(id=diff.episode_id).paths()
source.append(*playlist) source.append(*playlist)
self.log(type=Log.Type.start, source=source.id, diffusion=diff, self.log(type=Log.TYPE_START, source=source.id, diffusion=diff,
comment=str(diff)) comment=str(diff))
def cancel_diff(self, source, diff): def cancel_diff(self, source, diff):
diff.type = Diffusion.Type.cancel diff.type = Diffusion.TYPE_CANCEL
diff.save() diff.save()
self.log(type=Log.Type.cancel, source=source.id, diffusion=diff, self.log(type=Log.TYPE_CANCEL, source=source.id, diffusion=diff,
comment=str(diff)) comment=str(diff))
def sync(self): def sync(self):

View File

@ -1,11 +1,17 @@
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from .page import Page from .page import Page, PageQuerySet
from .program import Program, InProgramQuerySet from .program import Program, InProgramQuerySet
class ArticleQuerySet(InProgramQuerySet, PageQuerySet):
pass
class Article(Page): class Article(Page):
detail_url_name = 'article-detail'
program = models.ForeignKey( program = models.ForeignKey(
Program, models.SET_NULL, Program, models.SET_NULL,
verbose_name=_('program'), blank=True, null=True, verbose_name=_('program'), blank=True, null=True,
@ -17,7 +23,7 @@ class Article(Page):
'instead of a blog article'), 'instead of a blog article'),
) )
objects = InProgramQuerySet.as_manager() objects = ArticleQuerySet.as_manager()
class Meta: class Meta:
verbose_name = _('Article') verbose_name = _('Article')

View File

@ -1,9 +1,7 @@
import datetime import datetime
from enum import IntEnum
from django.db import models from django.db import models
from django.db.models import F, Q from django.db.models import Q
from django.db.models.functions import Concat, Substr
from django.utils import timezone as tz from django.utils import timezone as tz
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.functional import cached_property from django.utils.functional import cached_property
@ -64,7 +62,7 @@ class DiffusionQuerySet(BaseRerunQuerySet):
def on_air(self): def on_air(self):
""" On air diffusions """ """ On air diffusions """
return self.filter(type=Diffusion.Type.on_air) return self.filter(type=Diffusion.TYPE_ON_AIR)
def now(self, now=None, order=True): def now(self, now=None, order=True):
""" Diffusions occuring now """ """ Diffusions occuring now """
@ -132,20 +130,20 @@ class Diffusion(BaseRerun):
""" """
objects = DiffusionQuerySet.as_manager() objects = DiffusionQuerySet.as_manager()
class Type(IntEnum): TYPE_ON_AIR = 0x00
on_air = 0x00 TYPE_UNCONFIRMED = 0x01
unconfirmed = 0x01 TYPE_CANCEL = 0x02
cancel = 0x02 TYPE_CHOICES = (
(TYPE_ON_AIR, _('on air')),
(TYPE_UNCONFIRMED, _('not confirmed')),
(TYPE_CANCEL, _('cancelled')),
)
episode = models.ForeignKey( episode = models.ForeignKey(
Episode, models.CASCADE, Episode, models.CASCADE, verbose_name=_('episode'),
verbose_name=_('episode'),
) )
type = models.SmallIntegerField( type = models.SmallIntegerField(
verbose_name=_('type'), verbose_name=_('type'), default=TYPE_ON_AIR, choices=TYPE_CHOICES,
default=Type.on_air,
choices=[(int(y), _(x.replace('_', ' ')))
for x, y in Type.__members__.items()],
) )
start = models.DateTimeField(_('start')) start = models.DateTimeField(_('start'))
end = models.DateTimeField(_('end')) end = models.DateTimeField(_('end'))
@ -222,7 +220,7 @@ class Diffusion(BaseRerun):
# TODO: property? # TODO: property?
def is_live(self): def is_live(self):
""" True if Diffusion is live (False if there are sounds files). """ """ True if Diffusion is live (False if there are sounds files). """
return self.type == self.Type.on_air and \ return self.type == self.TYPE_ON_AIR and \
not self.episode.sound_set.archive().count() not self.episode.sound_set.archive().count()
def get_playlist(self, **types): def get_playlist(self, **types):
@ -232,7 +230,7 @@ class Diffusion(BaseRerun):
""" """
from .sound import Sound from .sound import Sound
return list(self.get_sounds(**types) return list(self.get_sounds(**types)
.filter(path__isnull=False, type=Sound.Type.archive) .filter(path__isnull=False, type=Sound.TYPE_ARCHIVE)
.values_list('path', flat=True)) .values_list('path', flat=True))
def get_sounds(self, **types): def get_sounds(self, **types):

View File

@ -1,6 +1,4 @@
from collections import deque from collections import deque
import datetime
from enum import IntEnum
import logging import logging
import os import os
@ -9,7 +7,7 @@ from django.utils import timezone as tz
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from aircox import settings, utils from aircox import settings
from .episode import Diffusion from .episode import Diffusion
from .sound import Sound, Track from .sound import Sound, Track
from .station import Station from .station import Station
@ -35,10 +33,10 @@ class LogQuerySet(models.QuerySet):
self.filter(date__date__gte=date) self.filter(date__date__gte=date)
def on_air(self): def on_air(self):
return self.filter(type=Log.Type.on_air) return self.filter(type=Log.TYPE_ON_AIR)
def start(self): def start(self):
return self.filter(type=Log.Type.start) return self.filter(type=Log.TYPE_START)
def with_diff(self, with_it=True): def with_diff(self, with_it=True):
return self.filter(diffusion__isnull=not with_it) return self.filter(diffusion__isnull=not with_it)
@ -163,43 +161,33 @@ class Log(models.Model):
This only remember what has been played on the outputs, not on each This only remember what has been played on the outputs, not on each
source; Source designate here which source is responsible of that. source; Source designate here which source is responsible of that.
""" """
class Type(IntEnum):
stop = 0x00 TYPE_STOP = 0x00
""" """ Source has been stopped, e.g. manually """
Source has been stopped, e.g. manually # Rule: \/ diffusion != null \/ sound != null
""" TYPE_START = 0x01
# Rule: \/ diffusion != null \/ sound != null """ Diffusion or sound has been request to be played. """
start = 0x01 TYPE_CANCEL = 0x02
""" Diffusion or sound has been request to be played. """ """ Diffusion has been canceled. """
cancel = 0x02 # Rule: \/ sound != null /\ track == null
""" Diffusion has been canceled. """ # \/ sound == null /\ track != null
# Rule: \/ sound != null /\ track == null # \/ sound == null /\ track == null /\ comment = sound_path
# \/ sound == null /\ track != null TYPE_ON_AIR = 0x03
# \/ sound == null /\ track == null /\ comment = sound_path """ Sound or diffusion occured on air """
on_air = 0x03 TYPE_OTHER = 0x04
""" """ Other log """
The sound or diffusion has been detected occurring on air. Can TYPE_CHOICES = (
also designate live diffusion, although Liquidsoap did not play (TYPE_STOP, _('stop')), (TYPE_START, _('start')),
them since they don't have an attached sound archive. (TYPE_CANCEL, _('cancelled')), (TYPE_ON_AIR, _('on air')),
""" (TYPE_OTHER, _('other'))
other = 0x04 )
""" Other log """
station = models.ForeignKey( station = models.ForeignKey(
Station, models.CASCADE, Station, models.CASCADE,
verbose_name=_('station'), verbose_name=_('station'), help_text=_('related station'),
help_text=_('related station'),
)
type = models.SmallIntegerField(
choices=[(int(y), _(x.replace('_', ' ')))
for x, y in Type.__members__.items()],
blank=True, null=True,
verbose_name=_('type'),
)
date = models.DateTimeField(
default=tz.now, db_index=True,
verbose_name=_('date'),
) )
type = models.SmallIntegerField(_('type'), choices=TYPE_CHOICES)
date = models.DateTimeField(_('date'), default=tz.now, db_index=True)
source = models.CharField( source = models.CharField(
# we use a CharField to avoid loosing logs information if the # we use a CharField to avoid loosing logs information if the
# source is removed # source is removed

View File

@ -38,28 +38,30 @@ class Category(models.Model):
class PageQuerySet(InheritanceQuerySet): class PageQuerySet(InheritanceQuerySet):
def draft(self): def draft(self):
return self.filter(status=Page.STATUS.draft) return self.filter(status=Page.STATUS_DRAFT)
def published(self): def published(self):
return self.filter(status=Page.STATUS.published) return self.filter(status=Page.STATUS_PUBLISHED)
def trash(self): def trash(self):
return self.filter(status=Page.STATUS.trash) return self.filter(status=Page.STATUS_TRASH)
class Page(models.Model): class Page(models.Model):
""" Base class for publishable content """ """ Base class for publishable content """
class STATUS(IntEnum): STATUS_DRAFT = 0x00
draft = 0x00 STATUS_PUBLISHED = 0x10
published = 0x10 STATUS_TRASH = 0x20
trash = 0x20 STATUS_CHOICES = (
(STATUS_DRAFT, _('draft')),
(STATUS_PUBLISHED, _('published')),
(STATUS_TRASH, _('trash')),
)
title = models.CharField(max_length=128) title = models.CharField(max_length=128)
slug = models.SlugField(_('slug'), blank=True, unique=True) slug = models.SlugField(_('slug'), blank=True, unique=True)
status = models.PositiveSmallIntegerField( status = models.PositiveSmallIntegerField(
_('status'), _('status'), default=STATUS_DRAFT, choices=STATUS_CHOICES,
default=STATUS.draft,
choices=[(int(y), _(x)) for x, y in STATUS.__members__.items()],
) )
category = models.ForeignKey( category = models.ForeignKey(
Category, models.SET_NULL, Category, models.SET_NULL,
@ -84,8 +86,6 @@ class Page(models.Model):
detail_url_name = None detail_url_name = None
class Meta:
abstract = True
def __str__(self): def __str__(self):
return '{}: {}'.format(self._meta.verbose_name, return '{}: {}'.format(self._meta.verbose_name,
@ -104,15 +104,15 @@ class Page(models.Model):
@property @property
def is_draft(self): def is_draft(self):
return self.status == self.STATUS.draft return self.status == self.STATUS_DRAFT
@property @property
def is_published(self): def is_published(self):
return self.status == self.STATUS.published return self.status == self.STATUS_PUBLISHED
@property @property
def is_trash(self): def is_trash(self):
return self.status == self.STATUS.trash return self.status == self.STATUS_TRASH
@cached_property @cached_property
def headline(self): def headline(self):
@ -132,6 +132,16 @@ class Page(models.Model):
return cls(**cls.get_init_kwargs_from(page, **kwargs)) return cls(**cls.get_init_kwargs_from(page, **kwargs))
class Comment(models.Model):
page = models.ForeignKey(
Page, models.CASCADE, verbose_name=_('related page'),
)
nickname = models.CharField(_('nickname'), max_length=32)
email = models.EmailField(_('email'), max_length=32)
date = models.DateTimeField(auto_now_add=True)
content = models.TextField(_('content'), max_length=1024)
class NavItem(models.Model): class NavItem(models.Model):
""" Navigation menu items """ """ Navigation menu items """
station = models.ForeignKey( station = models.ForeignKey(

View File

@ -45,6 +45,11 @@ class Program(Page):
Renaming a Program rename the corresponding directory to matches the new Renaming a Program rename the corresponding directory to matches the new
name if it does not exists. name if it does not exists.
""" """
# explicit foreign key in order to avoid related name clashes
page = models.OneToOneField(
Page, models.CASCADE,
parent_link=True, related_name='program_page'
)
station = models.ForeignKey( station = models.ForeignKey(
Station, Station,
verbose_name=_('station'), verbose_name=_('station'),
@ -478,7 +483,7 @@ class Schedule(BaseRerun):
initial = diffusions[initial] initial = diffusions[initial]
diffusions[date] = Diffusion( diffusions[date] = Diffusion(
episode=episode, type=Diffusion.Type.on_air, episode=episode, type=Diffusion.TYPE_ON_AIR,
initial=initial, start=date, end=date+duration initial=initial, start=date, end=date+duration
) )
return episodes.values(), diffusions.values() return episodes.values(), diffusions.values()

View File

@ -36,7 +36,7 @@ class SoundQuerySet(models.QuerySet):
def archive(self): def archive(self):
""" Return sounds that are archives """ """ Return sounds that are archives """
return self.filter(type=Sound.Type.archive) return self.filter(type=Sound.TYPE_ARCHIVE)
def paths(self, archive=True, order_by=True): def paths(self, archive=True, order_by=True):
""" """
@ -55,11 +55,14 @@ class Sound(models.Model):
A Sound is the representation of a sound file that can be either an excerpt A Sound is the representation of a sound file that can be either an excerpt
or a complete archive of the related diffusion. or a complete archive of the related diffusion.
""" """
class Type(IntEnum): TYPE_OTHER = 0x00
other = 0x00, TYPE_ARCHIVE = 0x01
archive = 0x01, TYPE_EXCERPT = 0x02
excerpt = 0x02, TYPE_REMOVED = 0x03
removed = 0x03, TYPE_CHOICES = (
(TYPE_OTHER, _('other')), (TYPE_ARCHIVE, _('archive')),
(TYPE_EXCERPT, _('excerpt')), (TYPE_REMOVED, _('removed'))
)
name = models.CharField(_('name'), max_length=64) name = models.CharField(_('name'), max_length=64)
program = models.ForeignKey( program = models.ForeignKey(
@ -72,11 +75,7 @@ class Sound(models.Model):
Episode, models.SET_NULL, blank=True, null=True, Episode, models.SET_NULL, blank=True, null=True,
verbose_name=_('episode'), verbose_name=_('episode'),
) )
type = models.SmallIntegerField( type = models.SmallIntegerField(_('type'), choices=TYPE_CHOICES)
verbose_name=_('type'),
choices=[(int(y), _(x)) for x, y in Type.__members__.items()],
blank=True, null=True
)
# FIXME: url() does not use the same directory than here # FIXME: url() does not use the same directory than here
# should we use FileField for more reliability? # should we use FileField for more reliability?
path = models.FilePathField( path = models.FilePathField(
@ -196,21 +195,21 @@ class Sound(models.Model):
""" """
if not self.file_exists(): if not self.file_exists():
if self.type == self.Type.removed: if self.type == self.TYPE_REMOVED:
return return
logger.info('sound %s: has been removed', self.path) logger.info('sound %s: has been removed', self.path)
self.type = self.Type.removed self.type = self.TYPE_REMOVED
return True return True
# not anymore removed # not anymore removed
changed = False changed = False
if self.type == self.Type.removed and self.program: if self.type == self.TYPE_REMOVED and self.program:
changed = True changed = True
self.type = self.Type.archive \ self.type = self.TYPE_ARCHIVE \
if self.path.startswith(self.program.archives_path) else \ if self.path.startswith(self.program.archives_path) else \
self.Type.excerpt self.TYPE_EXCERPT
# check mtime -> reset quality if changed (assume file changed) # check mtime -> reset quality if changed (assume file changed)
mtime = self.get_mtime() mtime = self.get_mtime()

View File

@ -1,4 +1,3 @@
from enum import IntEnum
import os import os
from django.db import models from django.db import models
@ -91,36 +90,32 @@ class Port(models.Model):
Some port types may be not available depending on the Some port types may be not available depending on the
direction of the port. direction of the port.
""" """
class Direction(IntEnum): DIRECTION_INPUT = 0x00
input = 0x00 DIRECTION_OUTPUT = 0x01
output = 0x01 DIRECTION_CHOICES = ((DIRECTION_INPUT, _('input')),
(DIRECTION_OUTPUT, _('output')))
class Type(IntEnum): TYPE_JACK = 0x00
jack = 0x00 TYPE_ALSA = 0x01
alsa = 0x01 TYPE_PULSEAUDIO = 0x02
pulseaudio = 0x02 TYPE_ICECAST = 0x03
icecast = 0x03 TYPE_HTTP = 0x04
http = 0x04 TYPE_HTTPS = 0x05
https = 0x05 TYPE_FILE = 0x06
file = 0x06 TYPE_CHOICES = (
(TYPE_JACK, 'jack'), (TYPE_ALSA, 'alsa'),
(TYPE_PULSEAUDIO, 'pulseaudio'), (TYPE_ICECAST, 'icecast'),
(TYPE_HTTP, 'http'), (TYPE_HTTPS, 'https'),
(TYPE_FILE, _('file'))
)
station = models.ForeignKey( station = models.ForeignKey(
Station, Station, models.CASCADE, verbose_name=_('station'))
verbose_name=_('station'),
on_delete=models.CASCADE,
)
direction = models.SmallIntegerField( direction = models.SmallIntegerField(
_('direction'), _('direction'), choices=DIRECTION_CHOICES)
choices=[(int(y), _(x)) for x, y in Direction.__members__.items()], type = models.SmallIntegerField(_('type'), choices=TYPE_CHOICES)
)
type = models.SmallIntegerField(
_('type'),
# we don't translate the names since it is project names.
choices=[(int(y), x) for x, y in Type.__members__.items()],
)
active = models.BooleanField( active = models.BooleanField(
_('active'), _('active'), default=True,
default=True,
help_text=_('this port is active') help_text=_('this port is active')
) )
settings = models.TextField( settings = models.TextField(
@ -136,13 +131,13 @@ class Port(models.Model):
Return True if the type is available for the given direction. Return True if the type is available for the given direction.
""" """
if self.direction == self.Direction.input: if self.direction == self.DIRECTION_INPUT:
return self.type not in ( return self.type not in (
self.Type.icecast, self.Type.file self.TYPE_ICECAST, self.TYPE_FILE
) )
return self.type not in ( return self.type not in (
self.Type.http, self.Type.https self.TYPE_HTTP, self.TYPE_HTTPS
) )
def save(self, *args, **kwargs): def save(self, *args, **kwargs):

View File

@ -7159,12 +7159,18 @@ label.panel-block {
.is-borderless { .is-borderless {
border: none; } border: none; }
.has-background-transparent {
background-color: transparent; }
.navbar + .container { .navbar + .container {
margin-top: 1em; } margin-top: 1em; }
.navbar.has-shadow, .navbar.is-fixed-bottom.has-shadow { .navbar.has-shadow, .navbar.is-fixed-bottom.has-shadow {
box-shadow: 0em 0em 1em rgba(0, 0, 0, 0.1); } box-shadow: 0em 0em 1em rgba(0, 0, 0, 0.1); }
a.navbar-item.is-active {
border-bottom: 1px grey solid; }
/* /*
.navbar-brand img { .navbar-brand img {
min-height: 6em; min-height: 6em;

View File

@ -419,7 +419,7 @@ eval("/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__,
/***/ (function(module, __webpack_exports__, __webpack_require__) { /***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict"; "use strict";
eval("/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"a\", function() { return render; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"b\", function() { return staticRenderFns; });\nvar render = function() {\n var _vm = this\n var _h = _vm.$createElement\n var _c = _vm._self._c || _h\n return _c(\n \"div\",\n [\n _c(\"div\", { staticClass: \"tabs is-centered\" }, [\n _c(\"ul\", [_vm._t(\"tabs\", null, { value: _vm.value })], 2)\n ]),\n _vm._v(\" \"),\n _vm._t(\"default\", null, { value: _vm.value })\n ],\n 2\n )\n}\nvar staticRenderFns = []\nrender._withStripped = true\n\n\n\n//# sourceURL=webpack:///./assets/vue/tabs.vue?./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options"); eval("/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"a\", function() { return render; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"b\", function() { return staticRenderFns; });\nvar render = function() {\n var _vm = this\n var _h = _vm.$createElement\n var _c = _vm._self._c || _h\n return _c(\n \"div\",\n [\n _c(\"div\", { staticClass: \"tabs is-centered is-medium\" }, [\n _c(\"ul\", [_vm._t(\"tabs\", null, { value: _vm.value })], 2)\n ]),\n _vm._v(\" \"),\n _vm._t(\"default\", null, { value: _vm.value })\n ],\n 2\n )\n}\nvar staticRenderFns = []\nrender._withStripped = true\n\n\n\n//# sourceURL=webpack:///./assets/vue/tabs.vue?./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options");
/***/ }) /***/ })

View File

@ -0,0 +1,32 @@
{% extends "admin/change_form.html" %}
{% load i18n static %}
{% block extrahead %}{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static "aircox/vendor.css" %}"/>
<link rel="stylesheet" type="text/css" href="{% static "aircox/main.css" %}"/>
{% endblock %}
{% block submit_buttons_bottom %}
{% if has_change_permission %}
<div class="columns is-size-5">
<div class="column has-text-left">
{% if original and not original.is_trash %}
<button type="submit" name="status" value="32" class="button is-danger is-size-6">{% trans "Move to trash" %}</button>
{% endif %}
{% if original and not original.is_draft %}
<button type="submit" name="status" value="0" class="button is-warning is-size-6">{% trans "Mark as draft" %}</button>
{% endif %}
</div>
<div class="column has-text-right">
<button type="submit" class="button is-secondary is-size-6">{% trans "Save" %}</button>
<button type="submit" name="_continue" class="button is-secondary is-size-6">{% trans "Save and continue" %}</button>
{% if not original.is_published %}
<button type="submit" name="status" value="16" class="button is-primary is-size-6">{% trans "Publish" %}</button>
{% endif %}
</div>
{% endif %}
</div>
{% endblock %}

View File

@ -0,0 +1,29 @@
{% extends "aircox/page.html" %}
{% load i18n %}
{% block side_nav %}
{{ block.super }}
{% if side_items %}
<section>
<h4 class="title is-4">{% trans "Latest news" %}</h4>
{% for object in side_items %}
{% include "aircox/page_item.html" %}
{% endfor %}
<br>
<nav class="pagination is-centered">
<ul class="pagination-list">
<li>
<a href="{% url "article-list" %}" class="pagination-link"
aria-label="{% trans "Show all news" %}">
{% trans "More news" %}
</a>
</li>
</ul>
</nav>
</section>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,12 @@
{% extends "aircox/page_list.html" %}
{% load i18n aircox %}
{% block title %}
{% if parent %}
{% with parent.title as parent %}
{% blocktrans %}Articles of {{ parent }}{% endblocktrans %}
{% endwith %}
{% else %}{{ block.super }}{% endif %}
{% endblock %}

View File

@ -67,6 +67,7 @@ Context:
{% block main %}{% endblock main %} {% block main %}{% endblock main %}
</main> </main>
{% if show_side_nav %} {% if show_side_nav %}
<aside class="column is-one-third-desktop"> <aside class="column is-one-third-desktop">
{% block cover %} {% block cover %}

View File

@ -1,20 +1,28 @@
{% extends "aircox/page.html" %} {% extends "aircox/page.html" %}
{% load i18n aircox %} {% load i18n aircox %}
{% block title %}{% trans "Timetable" %}{% endblock %} {% block title %}
{% with station.name as station %}
{% blocktrans %}This week's shows... {% endblocktrans %}
{% endwith %}
{% endblock %}
{% block subtitle %}
<div class="column">
{% blocktrans %}From <b>{{ start }}</b> to <b>{{ end }}</b>{% endblocktrans %}
</div>
{% endblock %}
{% block main %} {% block main %}
{{ block.super }} {{ block.super }}
<section class="section">
<h3 class="subtitle size-3">
{% blocktrans %}From <b>{{ start }}</b> to <b>{{ end }}</b>{% endblocktrans %}
</h3>
{% with True as hide_schedule %}
<section>
{% unique_id "timetable" as timetable_id %} {% unique_id "timetable" as timetable_id %}
<a-tabs default="{{ date }}"> <a-tabs default="{{ date }}">
<template v-slot:tabs="scope" noscript="hidden"> <template v-slot:tabs="scope" noscript="hidden">
<li><a href="{% url "timetable" date=prev_date %}">&lt;</a></li> <li><a href="{% url "timetable" date=prev_date %}">&#10092; {% trans "Before" %}</a></li>
{% for day in by_date.keys %} {% for day in by_date.keys %}
<a-tab value="{{ day }}"> <a-tab value="{{ day }}">
@ -25,11 +33,10 @@
{% endfor %} {% endfor %}
<li> <li>
<a href="{% url "timetable" date=next_date %}">&gt;</a> <a href="{% url "timetable" date=next_date %}">{% trans "After" %} &#10093;</a>
</li> </li>
</template> </template>
{% with True as hide_schedule %}
<template v-slot:default="{value}"> <template v-slot:default="{value}">
{% for day, diffusions in by_date.items %} {% for day, diffusions in by_date.items %}
<noscript><h4 class="subtitle is-4">{{ day|date:"l d F Y" }}</h4></noscript> <noscript><h4 class="subtitle is-4">{{ day|date:"l d F Y" }}</h4></noscript>
@ -51,8 +58,8 @@
</div> </div>
{% endfor %} {% endfor %}
</template> </template>
{% endwith %}
</a-tabs> </a-tabs>
</section> </section>
{% endwith %}
{% endblock %} {% endblock %}

View File

@ -32,9 +32,9 @@
{{ block.super }} {{ block.super }}
{% if podcasts or tracks %} {% if podcasts or tracks %}
<section class="columns is-desktop"> <div class="columns is-desktop">
{% if tracks %} {% if tracks %}
<div class="column"> <section class="column">
<h4 class="title is-4">{% trans "Playlist" %}</h4> <h4 class="title is-4">{% trans "Playlist" %}</h4>
<ol> <ol>
{% for track in tracks %} {% for track in tracks %}
@ -46,17 +46,17 @@
</li> </li>
{% endfor %} {% endfor %}
</ol> </ol>
</div> </section>
{% endif %} {% endif %}
{% if podcasts %} {% if podcasts %}
<div class="column"> <section class="column">
<h4 class="title is-4">{% trans "Podcasts" %}</h4> <h4 class="title is-4">{% trans "Podcasts" %}</h4>
{% for object in podcasts %} {% for object in podcasts %}
{% include "aircox/podcast_item.html" %} {% include "aircox/podcast_item.html" %}
{% endfor %} {% endfor %}
{% endif %} </section>
</div>
{% endif %} {% endif %}
</section> </div>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -2,13 +2,11 @@
{% load i18n aircox %} {% load i18n aircox %}
{% block title %} {% block title %}
{% if program %} {% if parent %}
{% with program.title as program %} {% with parent.title as parent %}
{% blocktrans %}Episodes of {{ program }}{% endblocktrans %} {% blocktrans %}Episodes of {{ parent }}{% endblocktrans %}
{% endwith %} {% endwith %}
{% else %} {% else %}{{ block.super }}{% endif %}
{% trans "Episodes" %}
{% endif %}
{% endblock %} {% endblock %}

View File

@ -11,7 +11,7 @@
<section class="section"> <section class="section">
{% if dates %} {% if dates %}
<nav class="tabs is-centered" aria-label="{% trans "Other days' logs" %}"> <nav class="tabs is-medium is-centered" aria-label="{% trans "Other days' logs" %}">
<ul> <ul>
{% for day in dates %} {% for day in dates %}
<li {% if day == date %}class="is-active"{% endif %}> <li {% if day == date %}class="is-active"{% endif %}>
@ -30,7 +30,7 @@
{# <h4 class="subtitle size-4">{{ date }}</h4> #} {# <h4 class="subtitle size-4">{{ date }}</h4> #}
{% with True as hide_schedule %} {% with True as hide_schedule %}
<table class="table is-striped is-hoverable is-fullwidth"> <table class="table is-striped is-hoverable is-fullwidth has-background-transparent">
{% for object in object_list %} {% for object in object_list %}
<tr> <tr>
<td> <td>

View File

@ -16,7 +16,7 @@ Context:
{% block head_title %} {% block head_title %}
{% block title %}{{ title }}{% endblock %} {% block title %}{{ title }}{% endblock %}
{% if title %} &dash; {% endif %} &mdash;
{{ station.name }} {{ station.name }}
{% endblock %} {% endblock %}

View File

@ -1,15 +1,14 @@
{% extends "aircox/page.html" %} {% extends "aircox/page.html" %}
{% load i18n aircox %} {% load i18n aircox %}
{% with view.model|verbose_name:True as model_name_plural %}
{% block title %} {% block title %}
{{ model_name_plural }} {{ view.model|verbose_name:True|title }}
{% endblock %} {% endblock %}
{% block side_nav %} {% block side_nav %}
{{ block.super }} {{ block.super }}
{% if filter_categories|length != 1 %}
<section class="toolbar"> <section class="toolbar">
<h4 class="subtitle is-5">{% trans "Filters" %}</h4> <h4 class="subtitle is-5">{% trans "Filters" %}</h4>
<form method="GET" action=""> <form method="GET" action="">
@ -51,6 +50,7 @@
</div> </div>
</form> </form>
</section> </section>
{% endif %}
{% endblock %} {% endblock %}
@ -97,5 +97,3 @@
{% endblock %} {% endblock %}
{% endwith %}

View File

@ -17,7 +17,7 @@
</noscript> </noscript>
<a-player ref="player" src="{{ audio_streams.0 }}" <a-player ref="player" src="{{ audio_streams.0 }}"
live-info-url="{% url "api-live" %}" live-info-timeout="15" live-info-url="{% url "api-live" %}" :live-info-timeout="15"
button-title="{% trans "Play/pause audio" %}"> button-title="{% trans "Play/pause audio" %}">
<template v-slot:sources> <template v-slot:sources>
{% for stream in audio_streams %} {% for stream in audio_streams %}

View File

@ -4,11 +4,11 @@
{% block side_nav %} {% block side_nav %}
{{ block.super }} {{ block.super }}
{% if episodes %} {% if side_items %}
<section> <section>
<h4 class="title is-4">{% trans "Last shows" %}</h4> <h4 class="title is-4">{% trans "Last shows" %}</h4>
{% for object in episodes %} {% for object in side_items %}
{% include "aircox/episode_item.html" %} {% include "aircox/episode_item.html" %}
{% endfor %} {% endfor %}
@ -16,13 +16,14 @@
<nav class="pagination is-centered"> <nav class="pagination is-centered">
<ul class="pagination-list"> <ul class="pagination-list">
<li> <li>
<a href="{% url "diffusion-list" program_slug=program.slug %}" <a href="{% url "diffusion-list" parent_slug=program.slug %}"
class="pagination-link" class="pagination-link"
aria-label="{% trans "Show all diffusions" %}"> aria-label="{% trans "Show all program's diffusions" %}">
{% trans "All shows" %} {% trans "More shows" %}
</a> </a>
</li> </li>
</ul> </ul>
</nav>
</section> </section>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -6,3 +6,39 @@
{% include "aircox/program_header.html" %} {% include "aircox/program_header.html" %}
{% endblock %} {% endblock %}
{% block main %}
{{ block.super }}
<br>
{% with show_headline=False %}
<div class="columns is-desktop">
{% if articles %}
<section class="column">
<h4 class="title is-4">{% trans "Articles" %}</h4>
{% for object in articles %}
{% include "aircox/page_item.html" %}
{% endfor %}
<br>
<nav class="pagination is-centered">
<ul class="pagination-list">
<li>
<a href="{% url "article-list" parent_slug=program.slug %}"
class="pagination-link"
aria-label="{% trans "Show all program's articles" %}">
{% trans "More articles" %}
</a>
</li>
</ul>
</nav>
</section>
{% endif %}
</div>
{% endwith %}
{% endblock %}

View File

@ -30,26 +30,26 @@ urls = [
views.ArticleListView.as_view(model=models.Article, is_static=False), views.ArticleListView.as_view(model=models.Article, is_static=False),
name='article-list'), name='article-list'),
path(_('articles/<slug:slug>/'), path(_('articles/<slug:slug>/'),
views.PageDetailView.as_view(model=models.Article), views.ArticleDetailView.as_view(),
name='article-detail'), name='article-detail'),
path(_('programs/'), views.PageListView.as_view(model=models.Program), path(_('programs/'), views.PageListView.as_view(model=models.Program),
name='program-list'), name='program-list'),
path(_('programs/<slug:slug>/'), path(_('programs/<slug:slug>/'),
views.ProgramDetailView.as_view(), name='program-detail'), views.ProgramDetailView.as_view(), name='program-detail'),
path(_('programs/<slug:program_slug>/episodes/'), path(_('programs/<slug:parent_slug>/episodes/'),
views.EpisodeListView.as_view(), name='diffusion-list'), views.EpisodeListView.as_view(), name='diffusion-list'),
path(_('programs/<slug:program_slug>/articles/'), path(_('programs/<slug:parent_slug>/articles/'),
views.ArticleListView.as_view(), name='article-list'), views.ArticleListView.as_view(), name='article-list'),
path(_('episodes/'), path(_('episodes/'),
views.EpisodeListView.as_view(), name='diffusion-list'), views.EpisodeListView.as_view(), name='diffusion-list'),
path(_('episodes/week/'),
views.TimetableView.as_view(), name='timetable'),
path(_('episodes/week/<week:date>/'),
views.TimetableView.as_view(), name='timetable'),
path(_('episodes/<slug:slug>/'), path(_('episodes/<slug:slug>/'),
views.EpisodeDetailView.as_view(), name='episode-detail'), views.EpisodeDetailView.as_view(), name='episode-detail'),
path(_('week/'),
views.TimetableView.as_view(), name='timetable'),
path(_('week/<week:date>/'),
views.TimetableView.as_view(), name='timetable'),
path(_('logs/'), views.LogListView.as_view(), name='logs'), path(_('logs/'), views.LogListView.as_view(), name='logs'),
path(_('logs/<date:date>/'), views.LogListView.as_view(), name='logs'), path(_('logs/<date:date>/'), views.LogListView.as_view(), name='logs'),

View File

@ -1,257 +0,0 @@
import os
import json
import datetime
from django.views.generic.base import View, TemplateResponseMixin
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponse, Http404
from django.shortcuts import render
from django.utils.translation import ugettext as _
from django.utils import timezone as tz
from django.views.decorators.cache import cache_page
import aircox.models as models
# FIXME usefull?
class Stations:
stations = models.Station.objects.all()
update_timeout = None
fetch_timeout = None
def fetch(self):
if self.fetch_timeout and self.fetch_timeout > tz.now():
return
self.fetch_timeout = tz.now() + tz.timedelta(seconds=5)
for station in self.stations:
station.streamer.fetch()
stations = Stations()
@cache_page(10)
def on_air(request):
try:
import aircox_cms.models as cms
except:
cms = None
station = request.GET.get('station')
if station:
# FIXME: by name???
station = stations.stations.filter(name=station)
if not station.count():
return HttpResponse('{}')
else:
station = stations.stations
station = station.first()
on_air = station.on_air(count=10).select_related('track', 'diffusion')
if not on_air.count():
return HttpResponse('')
last = on_air.first()
if last.track:
last = {'date': last.date, 'type': 'track',
'artist': last.track.artist, 'title': last.track.title}
else:
try:
diff = last.diffusion
publication = None
# FIXME CMS
if cms:
publication = \
cms.DiffusionPage.objects.filter(
diffusion=diff.initial or diff).first() or \
cms.ProgramPage.objects.filter(
program=last.program).first()
except:
pass
last = {'date': diff.start, 'type': 'diffusion',
'title': diff.program.name,
'url': publication.specific.url if publication else None}
last['date'] = str(last['date'])
return HttpResponse(json.dumps(last))
# TODO:
# - login url
class Monitor(View, TemplateResponseMixin, LoginRequiredMixin):
template_name = 'aircox/controllers/monitor.html'
def get_context_data(self, **kwargs):
stations.fetch()
return {'stations': stations.stations}
def get(self, request=None, **kwargs):
if not request.user.is_active:
return Http404()
self.request = request
context = self.get_context_data(**kwargs)
return render(request, self.template_name, context)
def post(self, request=None, **kwargs):
if not request.user.is_active:
return Http404()
if not ('action' or 'station') in request.POST:
return HttpResponse('')
POST = request.POST
POST.get('controller')
action = POST.get('action')
station = stations.stations.filter(name=POST.get('station')) \
.first()
if not station:
return Http404()
source = None
if 'source' in POST:
source = [s for s in station.sources
if s.name == POST['source']]
source = source[0]
if not source:
return Http404
station.streamer.fetch()
source = source or station.streamer.source
if action == 'skip':
self.actionSkip(request, station, source)
if action == 'restart':
self.actionRestart(request, station, source)
return HttpResponse('')
def actionSkip(self, request, station, source):
source.skip()
def actionRestart(self, request, station, source):
source.restart()
class StatisticsView(View, TemplateResponseMixin, LoginRequiredMixin):
"""
View for statistics.
"""
# we cannot manipulate queryset: we have to be able to read from archives
template_name = 'aircox/controllers/stats.html'
class Item:
date = None
end = None
name = None
related = None
tracks = None
tags = None
col = None
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
class Stats:
station = None
date = None
items = None
"""
Log or Diffusion object that has been diffused by date. These
objects have extra fields:
- tags: [ (tag_name, tag_count), ...]
- tracks_count: total count of tracks
"""
count = 0
#rows = None
def __init__(self, **kwargs):
self.items = []
# self.rows = []
self.__dict__.update(kwargs)
# Note: one row contains a column for diffusions and one for streams
# def append(self, log):
# if log.col == 0:
# self.rows.append((log, []))
# return
#
# if self.rows:
# row = self.rows[len(self.rows)-1]
# last = row[0] or row[1][len(row[1])-1]
# if last.date < log.date < last.end:
# row[1].append(log)
# return
#
# # all other cases: new row
# self.rows.append((None, [log]))
def get_stats(self, station, date):
"""
Return statistics for the given station and date.
"""
stats = self.Stats(station=station, date=date,
items=[], tags={})
qs = Log.objects.station(station).on_air() \
.prefetch_related('diffusion', 'sound', 'track', 'track__tags')
if not qs.exists():
qs = models.Log.objects.load_archive(station, date)
sound_log = None
for log in qs:
rel, item = None, None
if log.diffusion:
rel, item = log.diffusion, self.Item(
name=rel.program.name, type=_('Diffusion'), col=0,
tracks=models.Track.objects.filter(diffusion=log.diffusion)
.prefetch_related('tags'),
)
sound_log = None
elif log.sound:
rel, item = log.sound, self.Item(
name=rel.program.name + ': ' + os.path.basename(rel.path),
type=_('Stream'), col=1, tracks=[],
)
sound_log = item
elif log.track:
# append to last sound log
if not sound_log:
continue
sound_log.tracks.append(log.track)
sound_log.end = log.end
continue
item.date = log.date
item.end = log.end
item.related = rel
# stats.append(item)
stats.items.append(item)
return stats
def get_context_data(self, **kwargs):
context = {}
date = datetime.date.today()
try:
GET = self.request.GET
year = int(GET["year"]) if 'year' in GET else date.year
month = int(GET["month"]) if 'month' in GET else date.month
day = int(GET["day"]) if 'day' in GET else date.day
date = datetime.date(year, month, day)
except:
pass
context["statistics"] = [
self.get_stats(station, date)
for station in models.Station.objects.all()
]
return context
def get(self, request=None, **kwargs):
if not request.user.is_active:
return Http404()
self.request = request
context = self.get_context_data(**kwargs)
return render(request, self.template_name, context)

View File

@ -1,6 +1,6 @@
from . import api from . import api
from .article import ArticleListView from .article import ArticleDetailView, ArticleListView
from .base import BaseView from .base import BaseView
from .episode import EpisodeDetailView, EpisodeListView, TimetableView from .episode import EpisodeDetailView, EpisodeListView, TimetableView
from .log import LogListView from .log import LogListView

View File

@ -1,16 +1,36 @@
from ..models import Article from ..models import Article, Program
from .program import ProgramPageListView from .page import ParentMixin, PageDetailView, PageListView
__all__ = ['ArticleListView'] __all__ = ['ArticleDetailView', 'ArticleListView']
class ArticleListView(ProgramPageListView): class ArticleDetailView(PageDetailView):
show_side_nav = True
model = Article
def get_side_queryset(self):
qs = Article.objects.select_related('cover') \
.filter(is_static=False) \
.order_by('-date')
return qs
def get_context_data(self, **kwargs):
if self.object.program is not None:
kwargs.setdefault('parent', self.object.program)
return super().get_context_data(**kwargs)
class ArticleListView(ParentMixin, PageListView):
model = Article model = Article
template_name = 'aircox/article_list.html' template_name = 'aircox/article_list.html'
show_headline = True show_headline = True
is_static = False is_static = False
def get_queryset(self): parent_model = Program
return super().get_queryset(is_static=self.is_static) fk_parent = 'program'
def get_queryset(self):
return super().get_queryset().filter(is_static=self.is_static)

View File

@ -6,17 +6,20 @@ from django.views.generic.base import TemplateResponseMixin, ContextMixin
from ..utils import Redirect from ..utils import Redirect
__all__ = ['BaseView', 'PageView'] __all__ = ['BaseView']
class BaseView(TemplateResponseMixin, ContextMixin): class BaseView(TemplateResponseMixin, ContextMixin):
show_side_nav = False
""" Show side navigation """
title = None title = None
""" Page title """ """ Page title """
cover = None cover = None
""" Page cover """ """ Page cover """
show_side_nav = False
""" Show side navigation """
list_count = 5
""" Item count for small lists displayed on page. """
@property @property
def station(self): def station(self):
return self.request.station return self.request.station
@ -24,14 +27,24 @@ class BaseView(TemplateResponseMixin, ContextMixin):
def get_queryset(self): def get_queryset(self):
return super().get_queryset().station(self.station) return super().get_queryset().station(self.station)
def get_context_data(self, **kwargs): def get_side_queryset(self):
""" Return a queryset of items to render on the side nav. """
return None
def get_context_data(self, side_items=None, **kwargs):
kwargs.setdefault('station', self.station) kwargs.setdefault('station', self.station)
kwargs.setdefault('cover', self.cover) kwargs.setdefault('cover', self.cover)
kwargs.setdefault('show_side_nav', self.show_side_nav)
show_side_nav = kwargs.setdefault('show_side_nav', self.show_side_nav)
if show_side_nav and side_items is None:
side_items = self.get_side_queryset()
side_items = None if side_items is None else \
side_items[:self.list_count]
if not 'audio_streams' in kwargs: if not 'audio_streams' in kwargs:
streams = self.station.audio_streams streams = self.station.audio_streams
streams = streams and streams.split('\n') streams = streams and streams.split('\n')
kwargs['audio_streams'] = streams kwargs['audio_streams'] = streams
return super().get_context_data(**kwargs)
return super().get_context_data(side_items=side_items, **kwargs)

View File

@ -1,16 +1,16 @@
from collections import OrderedDict from collections import OrderedDict
import datetime import datetime
from django.db.models import OuterRef, Subquery
from django.shortcuts import get_object_or_404
from django.views.generic import ListView from django.views.generic import ListView
from ..models import Diffusion, Episode, Page, Program, Sound from ..converters import WeekConverter
from ..models import Diffusion, Episode, Program, Sound
from .base import BaseView from .base import BaseView
from .program import ProgramPageDetailView, ProgramPageListView from .program import ProgramPageDetailView
from .page import ParentMixin, PageListView
__all__ = ['EpisodeDetailView', 'DiffusionListView', 'TimetableView'] __all__ = ['EpisodeDetailView', 'EpisodeListView', 'TimetableView']
class EpisodeDetailView(ProgramPageDetailView): class EpisodeDetailView(ProgramPageDetailView):
@ -20,8 +20,9 @@ class EpisodeDetailView(ProgramPageDetailView):
return Sound.objects.diffusion(diffusion).podcasts() return Sound.objects.diffusion(diffusion).podcasts()
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs.setdefault('program', self.object.program) self.program = kwargs.setdefault('program', self.object.program)
kwargs.setdefault('parent', kwargs['program'])
kwargs.setdefault('parent', self.program)
if not 'tracks' in kwargs: if not 'tracks' in kwargs:
kwargs['tracks'] = self.object.track_set.order_by('position') kwargs['tracks'] = self.object.track_set.order_by('position')
if not 'podcasts' in kwargs: if not 'podcasts' in kwargs:
@ -29,12 +30,15 @@ class EpisodeDetailView(ProgramPageDetailView):
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
class EpisodeListView(ProgramPageListView): class EpisodeListView(ParentMixin, PageListView):
model = Episode model = Episode
template_name = 'aircox/diffusion_list.html' template_name = 'aircox/diffusion_list.html'
item_template_name = 'aircox/episode_item.html' item_template_name = 'aircox/episode_item.html'
show_headline = True show_headline = True
parent_model = Program
fk_parent = 'program'
class TimetableView(BaseView, ListView): class TimetableView(BaseView, ListView):
""" View for timetables """ """ View for timetables """

View File

@ -1,6 +1,7 @@
from django.core.exceptions import FieldDoesNotExist from django.core.exceptions import FieldDoesNotExist
from django.http import Http404 from django.http import Http404
from django.shortcuts import get_object_or_404
from django.views.generic import DetailView, ListView from django.views.generic import DetailView, ListView
from ..models import Category from ..models import Category
@ -8,41 +9,48 @@ from ..utils import Redirect
from .base import BaseView from .base import BaseView
__all__ = ['PageDetailView', 'PageListView'] __all__ = ['ParentMixin', 'PageDetailView', 'PageListView']
class PageDetailView(BaseView, DetailView): class ParentMixin:
""" Base view class for pages. """ """
context_object_name = 'page' Optional parent page for a list view. Parent is fetched and passed to the
template context when `parent_model` is provided (queryset is filtered by
parent page in such case).
"""
parent_model = None
""" Parent model """
parent_url_kwarg = 'parent_slug'
""" Url lookup argument """
parent_field = 'slug'
""" Parent field for url lookup """
fk_parent = 'page'
""" Page foreign key to the parent """
parent = None
""" Parent page object """
def get_parent(self, request, *args, **kwargs):
if self.parent_model is None or self.parent_url_kwarg not in kwargs:
return
lookup = {self.parent_field: kwargs[self.parent_url_kwarg]}
return get_object_or_404(
self.parent_model.objects.select_related('cover'), **lookup)
def get(self, request, *args, **kwargs):
self.parent = self.get_parent(request, *args, **kwargs)
return super().get(request, *args, **kwargs)
def get_queryset(self): def get_queryset(self):
return super().get_queryset().select_related('cover', 'category') if self.parent is not None:
lookup = {self.fk_parent: self.parent}
# This should not exists: it allows mapping not published pages return super().get_queryset().filter(**lookup)
# or it should be only used for trashed pages. return super().get_queryset()
def not_published_redirect(self, page):
"""
When a page is not published, redirect to the returned url instead
of an HTTP 404 code. """
return None
def get_object(self):
obj = super().get_object()
if not obj.is_published:
redirect_url = self.not_published_redirect(obj)
if redirect_url:
raise Redirect(redirect_url)
raise Http404('%s not found' % self.model._meta.verbose_name)
return obj
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
#if kwargs.get('regions') is None: parent = kwargs.setdefault('parent', self.parent)
# contents = contents_for_item( if parent is not None:
# page, page_renderer._renderers.keys()) kwargs.setdefault('cover', parent.cover)
# kwargs['regions'] = contents.render_regions(page_renderer)
page = kwargs.setdefault('page', self.object)
kwargs.setdefault('title', page.title)
kwargs.setdefault('cover', page.cover)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
@ -84,4 +92,35 @@ class PageListView(BaseView, ListView):
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
class PageDetailView(BaseView, DetailView):
""" Base view class for pages. """
context_object_name = 'page'
def get_queryset(self):
return super().get_queryset().select_related('cover', 'category')
# This should not exists: it allows mapping not published pages
# or it should be only used for trashed pages.
def not_published_redirect(self, page):
"""
When a page is not published, redirect to the returned url instead
of an HTTP 404 code. """
return None
def get_object(self):
obj = super().get_object()
if not obj.is_published:
redirect_url = self.not_published_redirect(obj)
if redirect_url:
raise Redirect(redirect_url)
raise Http404('%s not found' % self.model._meta.verbose_name)
return obj
def get_context_data(self, **kwargs):
page = kwargs.setdefault('page', self.object)
kwargs.setdefault('title', page.title)
kwargs.setdefault('cover', page.cover)
return super().get_context_data(**kwargs)

View File

@ -12,53 +12,25 @@ class ProgramPageDetailView(PageDetailView):
""" """
Base view class for a page that is displayed as a program's child page. Base view class for a page that is displayed as a program's child page.
""" """
program = None
show_side_nav = True show_side_nav = True
list_count = 5 list_count = 5
def get_episodes_queryset(self, program): def get_side_queryset(self):
return program.episode_set.published().order_by('-date') return self.program.episode_set.published().order_by('-date')
def get_context_data(self, program, episodes=None, **kwargs):
if episodes is None:
episodes = self.get_episodes_queryset(program)
return super().get_context_data(
program=program, episodes=episodes[:self.list_count], **kwargs)
class ProgramPageListView(PageListView):
"""
Base list view class rendering pages as a program's child page.
Retrieved program from it slug provided by `kwargs['program_slug']`.
This view class can be used with or without providing a program.
"""
program = None
def get(self, request, *args, **kwargs):
slug = kwargs.get('program_slug', None)
if slug is not None:
self.program = get_object_or_404(
Program.objects.select_related('cover'), slug=slug)
return super().get(request, *args, **kwargs)
def get_queryset(self):
return super().get_queryset().filter(program=self.program) \
if self.program else super().get_queryset()
def get_context_data(self, **kwargs):
program = kwargs.setdefault('program', self.program)
if program is not None:
kwargs.setdefault('cover', program.cover)
kwargs.setdefault('parent', program)
return super().get_context_data(**kwargs)
class ProgramDetailView(ProgramPageDetailView): class ProgramDetailView(ProgramPageDetailView):
model = Program model = Program
def get_articles_queryset(self, program): def get_articles_queryset(self):
return program.article_set.published().order_by('-date') return self.program.article_set.published().order_by('-date')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs.setdefault('program', self.object) self.program = kwargs.setdefault('program', self.object)
if 'articles' not in kwargs:
kwargs['articles'] = \
self.get_articles_queryset()[:self.list_count]
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)

View File

View File

@ -1,81 +0,0 @@
import copy
from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
from content_editor.admin import ContentEditor, ContentEditorInline
from aircox import models as aircox
from . import models
from aircox.admin.playlist import TracksInline
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),
ContentEditorInline.create(models.SiteLink),
]
class PageDiffusionPlaylist(UnrelatedInlineMixin, TracksInline):
parent_model = aircox.Diffusion
fields = list(TracksInline.fields)
fields.remove('timestamp')
def get_parent(self, view_obj):
return view_obj and view_obj.diffusion
def save_parent(self, parent, view_obj):
parent.save()
view_obj.diffusion = parent
view_obj.save()
@admin.register(models.Page)
class PageAdmin(ContentEditor):
fieldsets = (
(_('Main'), {
'fields': ['title', 'slug', 'cover', 'headline'],
'classes': ('tabbed', 'uncollapse')
}),
(_('Settings'), {
'fields': ['featured', 'as_program', 'allow_comments', 'status'],
'classes': ('tabbed',)
}),
)
list_display = ["title", "status", "slug"]
list_editable = ['status']
prepopulated_fields = {"slug": ("title",)}
inlines = [
ContentEditorInline.create(models.PageRichText),
ContentEditorInline.create(models.PageImage),
]
@admin.register(models.DiffusionPage)
class DiffusionPageAdmin(PageAdmin):
fieldsets = copy.deepcopy(PageAdmin.fieldsets)
fieldsets[1][1]['fields'].insert(0, 'diffusion')
# 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
@admin.register(models.ProgramPage)
class ProgramPageAdmin(PageAdmin):
fieldsets = copy.deepcopy(PageAdmin.fieldsets)
fieldsets[1][1]['fields'].insert(0, 'program')
prepopulated_fields = {}
readonly_fields = ['slug']

View File

@ -1,5 +0,0 @@
from django.apps import AppConfig
class AircoxWebConfig(AppConfig):
name = 'aircox_web'

View File

@ -1,5 +0,0 @@
import './js';
import './styles.scss';
import './noscript.scss';
import './vue';

View File

@ -1,14 +0,0 @@
import Vue from 'vue';
import Buefy from 'buefy';
Vue.use(Buefy);
window.addEventListener('load', () => {
var app = new Vue({
el: '#app',
delimiters: [ '[[', ']]' ],
})
});

View File

@ -1,75 +0,0 @@
@charset "utf-8";
@import "~bulma/sass/utilities/_all.sass";
$body-background-color: $light;
@import "~bulma/bulma";
.navbar {
margin-bottom: 1em;
}
.navbar.has-shadow {
box-shadow: 0em 0.05em 0.5em rgba(0,0,0,0.1);
}
/*
.navbar-brand img {
min-height: 6em;
}
.navbar-menu .navbar-item:not(:last-child) {
border-right: 1px $grey solid;
}
*/
/** page **/
.page {
& > .cover {
float: right;
max-width: 45%;
}
& > .header {
margin-bottom: 1.5em;
}
.headline {
font-size: 1.4em;
padding: 0.2em 0em;
}
p {
padding: 0.4em 0em;
}
}
.cover {
margin: 1em 0em;
border: 0.2em black solid;
}
.small-cover {
width: 10em;
}
aside {
.small-cover {
width: 4em;
}
.media .subtitle {
font-size: 1em;
}
.media .content {
display: none;
}
}

View File

@ -1,11 +0,0 @@
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};

View File

@ -1,31 +0,0 @@
<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>

View File

@ -1,45 +0,0 @@
<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>

View File

@ -1,32 +0,0 @@
from django.db import models
class BaseMinMaxField:
def __init__(self, verbose_name=None, name=None, min=None, max=None,
**kwargs):
super().__init__(verbose_name, name, **kwargs)
self.min_value = min
self.max_value = max
def minmax(self, value):
return min(self.max_value, max(self.min_value, value))
def to_python(self, value):
return self.minmax(super().to_python(value))
def get_prep_value(self, value):
return super().get_prep_value(self.minmax(value))
class MinMaxField(BaseMinMaxField, models.IntegerField):
pass
class SmallMinMaxField(BaseMinMaxField, models.SmallIntegerField):
pass
class PositiveMinMaxField(BaseMinMaxField, models.PositiveIntegerField):
pass
class PositiveSmallMinMaxField(BaseMinMaxField, models.PositiveSmallIntegerField):
pass

View File

@ -1,214 +0,0 @@
from django.db import models
from django.db.models import F, Q
from django.urls import reverse
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 InheritanceQuerySet
from model_utils import Choices
from filer.fields.image import FilerImageField
from aircox import models as aircox
from . import plugins
class Site(models.Model):
station = models.ForeignKey(
aircox.Station, on_delete=models.SET_NULL, null=True,
)
#hosts = models.TextField(
# _('hosts'),
# help_text=_('website addresses (one per line)'),
#)
# main settings
title = models.CharField(
_('title'), max_length=32,
help_text=_('website title displayed to users'),
)
logo = FilerImageField(
on_delete=models.SET_NULL, null=True, blank=True,
verbose_name=_('logo'),
related_name='+',
)
favicon = FilerImageField(
on_delete=models.SET_NULL, null=True, blank=True,
verbose_name=_('favicon'),
related_name='+',
)
default = models.BooleanField(_('is default'),
default=False,
help_text=_('use this website by default'),
)
# meta descriptors
description = models.CharField(
_('description'), max_length=128,
blank=True, null=True,
)
tags = models.CharField(
_('tags'), max_length=128,
blank=True, null=True,
)
regions = [
Region(key='topnav', title=_('Navigation'), inherited=True),
Region(key='sidenav', title=_('Side Navigation'), inherited=True),
]
SitePlugin = create_plugin_base(Site)
class SiteRichText(plugins.RichText, SitePlugin):
pass
class SiteImage(plugins.Image, SitePlugin):
pass
class SiteLink(plugins.Link, SitePlugin):
css_class="navbar-item"
#-----------------------------------------------------------------------
class PageQueryset(InheritanceQuerySet):
def live(self):
return self.filter(status=Page.STATUS.published)
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', 'published', 'trash')
regions = [
Region(key="content", title=_("Content")),
]
title = models.CharField(max_length=128)
slug = models.SlugField(_('slug'), blank=True, unique=True)
headline = models.TextField(
_('headline'), max_length=128, blank=True, null=True,
)
# content
as_program = models.ForeignKey(
aircox.Program, models.SET_NULL, blank=True, null=True,
related_name='+',
# SO#51948640
# limit_choices_to={'schedule__isnull': False},
verbose_name=_('Show program as author'),
help_text=_("Show program as author"),
)
cover = FilerImageField(
on_delete=models.SET_NULL, null=True, blank=True,
verbose_name=_('Cover'),
related_name='+',
)
# options
featured = models.BooleanField(
_('featured'), default=False,
)
allow_comments = models.BooleanField(
_('allow comments'), default=True,
)
objects = PageQueryset.as_manager()
@property
def is_published(self):
return self.status == self.STATUS.published
@property
def path(self):
return reverse(self.detail_url_name, kwargs={'slug': self.slug})
def get_view_class(self):
""" Page view class"""
raise NotImplementedError('not implemented')
def view(self, request, *args, site=None, **kwargs):
""" Page view function """
view = self.get_view_class().as_view(site=site, page=self)
return view(request, *args, **kwargs)
def __str__(self):
return '{}: {}'.format(self._meta.verbose_name,
self.title or self.pk)
class DiffusionPage(Page):
detail_url_name = 'diffusion-page'
diffusion = models.OneToOneField(
aircox.Diffusion, models.CASCADE,
related_name='page',
limit_choices_to={'initial__isnull': True}
)
@property
def path(self):
return reverse('diffusion-page', kwargs={'slug': self.slug})
def save(self, *args, **kwargs):
program = self.diffusion.program
self.as_program = self.diffusion.program
if not self.slug.startswith(program.slug + '-'):
self.slug = '{}-{}'.format(program.slug, self.slug)
return super().save(*args, **kwargs)
def get_diffusions_with_page(queryset=aircox.Diffusion.objects,
status=Page.STATUS.published):
return queryset.filter(Q(page__isnull=True) |
Q(initial__page__isnull=True),
Q(page__status=status) |
Q(initial__page__status=status))
class ProgramPage(Page):
detail_url_name = 'program-page'
program = models.OneToOneField(
aircox.Program, models.CASCADE,
related_name='page',
)
@property
def path(self):
return reverse('program-page', kwargs={'slug': self.slug})
def save(self, *args, **kwargs):
self.slug = self.program.slug
return super().save(*args, **kwargs)
#-----------------------------------------------------------------------
PagePlugin = create_plugin_base(Page)
class PageRichText(plugins.RichText, PagePlugin):
pass
class PageImage(plugins.Image, PagePlugin):
pass

View File

@ -1,29 +0,0 @@
{
"name": "aircox-web-assets",
"version": "0.0.0",
"description": "Assets for Aircox Web",
"main": "index.js",
"author": "bkfox",
"license": "AGPL",
"devDependencies": {
"@fortawesome/fontawesome-free": "^5.8.2",
"bulma": "^0.7.5",
"css-loader": "^2.1.1",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"file-loader": "^3.0.1",
"mini-css-extract-plugin": "^0.5.0",
"node-sass": "^4.12.0",
"sass-loader": "^7.1.0",
"style-loader": "^0.23.1",
"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"
},
"dependencies": {
"buefy": "^0.7.8",
"vue": "^2.6.10"
}
}

View File

@ -1,51 +0,0 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils.html import escape, format_html, mark_safe
from django.urls import reverse
from .image import ImageBase, Image
from .richtext import RichText
__all__ = ['ImageBase', 'Image', 'RichText']
class Link(models.Model):
url = models.CharField(
_('url'), max_length=128, null=True, blank=True,
)
page = models.ForeignKey(
'Page', models.SET_NULL, null=True, blank=True,
verbose_name=_('Link to a page')
)
text = models.CharField(_('text'), max_length=64, null=True, blank=True)
info = models.CharField(_('info'), max_length=128, null=True, blank=True,
help_text=_('link description displayed as tooltip'))
blank = models.BooleanField(_('new window'), default=False,
help_text=_('open in a new window'))
css_class=""
def get_url(self):
if self.page:
return self.page.path #reverse('page', args=[self.page.path])
return self.url or ''
def render(self):
# FIXME: quote
return format_html(
'<a href="{}" title="{}"{}>{}</a>',
self.get_url(), escape(self.info),
' class=' + escape(self.css_class) + ''
if self.css_class else '',
self.text or (self.page and self.page.title) or '',
)
class Meta:
abstract = True
class Search(models.Model):
class Meta:
abstract = True

View File

@ -1,47 +0,0 @@
from django.db import models
from django.templatetags.static import static
from django.utils.translation import ugettext_lazy as _
from django.utils.html import format_html, mark_safe
from easy_thumbnails.files import get_thumbnailer
from filer.fields.image import FilerImageField
__all__ = ['ImageBase', 'Image']
class ImageBase(models.Model):
image = FilerImageField(
on_delete=models.CASCADE,
verbose_name=_('image'),
)
width = None
height = None
crop = False
class Meta:
abstract = True
@property
def thumbnail(self):
if self.width == None and self.height == None:
return self.image
opts = {}
if self.crop:
opts['crop'] = 'smart'
opts['size'] = (self.width or 0, self.height or 0)
thumbnailer = get_thumbnailer(self.image)
return thumbnailer.get_thumbnail(opts)
def render(self):
return format_html('<img src="{}" alt=""/>', self.thumbnail.url)
class Image(ImageBase):
width = models.PositiveSmallIntegerField(blank=True,null=True)
height = models.PositiveSmallIntegerField(blank=True,null=True)
crop = models.BooleanField(default=False)
class Meta:
abstract = True

View File

@ -1,12 +0,0 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
from ckeditor.fields import RichTextField
class RichText(models.Model):
text = RichTextField(_('text'))
class Meta:
abstract = True

View File

@ -1,37 +0,0 @@
import datetime
from django.db import models
from django.templatetags.static import static
from django.utils.translation import ugettext_lazy as _
from aircox import models as aircox
from aircox_web.fields import PositiveSmallMinMaxField
class Timetable(models.Model):
station = models.ForeignKey(
aircox.Station, models.CASCADE, verbose_name=_('station'),
)
days_before = models.PositiveSmallMinMaxField(
_('days before'), min=0, max=6,
help_text=_('Count of days displayed current date'),
)
days_after = models.PositiveSmallMinMaxField(
_('days after'), min=0, max=6,
help_text=_('Count of days displayed current date'),
)
def get_queryset(self, date=None):
date = date if date is not None else datetime.date.today()
qs = aircox.Diffusion.objects.station(self.station)
if self.days_before is None and self.days_after is None:
return qs.at(date)
start = date - datetime.timedelta(days=self.days_before) \
if self.days_before else date
stop = date + datetime.timedelta(days=self.days_after) \
if self.days_after else date
return aircox.Diffusion.objects.station(self.station) \
.after(start).before(stop)

View File

@ -1,18 +0,0 @@
from django.utils.html import format_html, mark_safe
from content_editor.renderer import PluginRenderer
from .models import *
site_renderer = PluginRenderer()
site_renderer._renderers.clear()
site_renderer.register(SiteRichText, lambda plugin: mark_safe(plugin.text))
site_renderer.register(SiteImage, lambda plugin: plugin.render())
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())

File diff suppressed because it is too large Load Diff

View File

@ -1,347 +0,0 @@
/******/ (function(modules) { // webpackBootstrap
/******/ // install a JSONP callback for chunk loading
/******/ function webpackJsonpCallback(data) {
/******/ var chunkIds = data[0];
/******/ var moreModules = data[1];
/******/ var executeModules = data[2];
/******/
/******/ // add "moreModules" to the modules object,
/******/ // then flag all "chunkIds" as loaded and fire callback
/******/ var moduleId, chunkId, i = 0, resolves = [];
/******/ for(;i < chunkIds.length; i++) {
/******/ chunkId = chunkIds[i];
/******/ if(installedChunks[chunkId]) {
/******/ resolves.push(installedChunks[chunkId][0]);
/******/ }
/******/ installedChunks[chunkId] = 0;
/******/ }
/******/ for(moduleId in moreModules) {
/******/ if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
/******/ modules[moduleId] = moreModules[moduleId];
/******/ }
/******/ }
/******/ if(parentJsonpFunction) parentJsonpFunction(data);
/******/
/******/ while(resolves.length) {
/******/ resolves.shift()();
/******/ }
/******/
/******/ // add entry modules from loaded chunk to deferred list
/******/ deferredModules.push.apply(deferredModules, executeModules || []);
/******/
/******/ // run deferred modules when all chunks ready
/******/ return checkDeferredModules();
/******/ };
/******/ function checkDeferredModules() {
/******/ var result;
/******/ for(var i = 0; i < deferredModules.length; i++) {
/******/ var deferredModule = deferredModules[i];
/******/ var fulfilled = true;
/******/ for(var j = 1; j < deferredModule.length; j++) {
/******/ var depId = deferredModule[j];
/******/ if(installedChunks[depId] !== 0) fulfilled = false;
/******/ }
/******/ if(fulfilled) {
/******/ deferredModules.splice(i--, 1);
/******/ result = __webpack_require__(__webpack_require__.s = deferredModule[0]);
/******/ }
/******/ }
/******/
/******/ return result;
/******/ }
/******/
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // object to store loaded and loading chunks
/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched
/******/ // Promise = chunk loading, 0 = chunk loaded
/******/ var installedChunks = {
/******/ "main": 0
/******/ };
/******/
/******/ var deferredModules = [];
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/ var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
/******/ var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
/******/ jsonpArray.push = webpackJsonpCallback;
/******/ jsonpArray = jsonpArray.slice();
/******/ for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
/******/ var parentJsonpFunction = oldJsonpFunction;
/******/
/******/
/******/ // add entry module to deferred list
/******/ deferredModules.push(["./assets/index.js","vendor"]);
/******/ // run deferred modules when ready
/******/ return checkDeferredModules();
/******/ })
/************************************************************************/
/******/ ({
/***/ "./assets/index.js":
/*!*************************!*\
!*** ./assets/index.js ***!
\*************************/
/*! no exports provided */
/*! all exports used */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./js */ \"./assets/js/index.js\");\n/* harmony import */ var _styles_scss__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./styles.scss */ \"./assets/styles.scss\");\n/* harmony import */ var _styles_scss__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_styles_scss__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var _noscript_scss__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./noscript.scss */ \"./assets/noscript.scss\");\n/* harmony import */ var _noscript_scss__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_noscript_scss__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var _vue__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./vue */ \"./assets/vue/index.js\");\n\n\n\n\n\n\n\n//# sourceURL=webpack:///./assets/index.js?");
/***/ }),
/***/ "./assets/js/index.js":
/*!****************************!*\
!*** ./assets/js/index.js ***!
\****************************/
/*! no exports provided */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm.browser.js\");\n/* harmony import */ var buefy__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! buefy */ \"./node_modules/buefy/dist/buefy.js\");\n/* harmony import */ var buefy__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(buefy__WEBPACK_IMPORTED_MODULE_1__);\n\n\n\nvue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].use(buefy__WEBPACK_IMPORTED_MODULE_1___default.a);\n\nwindow.addEventListener('load', () => {\n var app = new vue__WEBPACK_IMPORTED_MODULE_0__[\"default\"]({\n el: '#app',\n delimiters: [ '[[', ']]' ],\n })\n});\n\n\n\n\n\n//# sourceURL=webpack:///./assets/js/index.js?");
/***/ }),
/***/ "./assets/noscript.scss":
/*!******************************!*\
!*** ./assets/noscript.scss ***!
\******************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
eval("// extracted by mini-css-extract-plugin\n\n//# sourceURL=webpack:///./assets/noscript.scss?");
/***/ }),
/***/ "./assets/styles.scss":
/*!****************************!*\
!*** ./assets/styles.scss ***!
\****************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
eval("// extracted by mini-css-extract-plugin\n\n//# sourceURL=webpack:///./assets/styles.scss?");
/***/ }),
/***/ "./assets/vue/index.js":
/*!*****************************!*\
!*** ./assets/vue/index.js ***!
\*****************************/
/*! exports provided: Tab, Tabs */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm.browser.js\");\n/* harmony import */ var _tab_vue__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./tab.vue */ \"./assets/vue/tab.vue\");\n/* harmony import */ var _tabs_vue__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./tabs.vue */ \"./assets/vue/tabs.vue\");\n\n\n\n\n\nvue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].component('a-tab', _tab_vue__WEBPACK_IMPORTED_MODULE_1__[/* default */ \"a\"]);\nvue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].component('a-tabs', _tabs_vue__WEBPACK_IMPORTED_MODULE_2__[/* default */ \"a\"]);\n\n\n\n\n\n\n//# sourceURL=webpack:///./assets/vue/index.js?");
/***/ }),
/***/ "./assets/vue/tab.vue":
/*!****************************!*\
!*** ./assets/vue/tab.vue ***!
\****************************/
/*! exports provided: default */
/*! exports used: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("/* harmony import */ var _tab_vue_vue_type_template_id_65401e0e___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./tab.vue?vue&type=template&id=65401e0e& */ \"./assets/vue/tab.vue?vue&type=template&id=65401e0e&\");\n/* harmony import */ var _tab_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./tab.vue?vue&type=script&lang=js& */ \"./assets/vue/tab.vue?vue&type=script&lang=js&\");\n/* harmony import */ var _node_modules_vue_loader_lib_runtime_componentNormalizer_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../node_modules/vue-loader/lib/runtime/componentNormalizer.js */ \"./node_modules/vue-loader/lib/runtime/componentNormalizer.js\");\n\n\n\n\n\n/* normalize component */\n\nvar component = Object(_node_modules_vue_loader_lib_runtime_componentNormalizer_js__WEBPACK_IMPORTED_MODULE_2__[/* default */ \"a\"])(\n _tab_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__[/* default */ \"a\"],\n _tab_vue_vue_type_template_id_65401e0e___WEBPACK_IMPORTED_MODULE_0__[/* render */ \"a\"],\n _tab_vue_vue_type_template_id_65401e0e___WEBPACK_IMPORTED_MODULE_0__[/* staticRenderFns */ \"b\"],\n false,\n null,\n null,\n null\n \n)\n\n/* hot reload */\nif (false) { var api; }\ncomponent.options.__file = \"assets/vue/tab.vue\"\n/* harmony default export */ __webpack_exports__[\"a\"] = (component.exports);\n\n//# sourceURL=webpack:///./assets/vue/tab.vue?");
/***/ }),
/***/ "./assets/vue/tab.vue?vue&type=script&lang=js&":
/*!*****************************************************!*\
!*** ./assets/vue/tab.vue?vue&type=script&lang=js& ***!
\*****************************************************/
/*! exports provided: default */
/*! exports used: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("/* harmony import */ var _node_modules_vue_loader_lib_index_js_vue_loader_options_tab_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../node_modules/vue-loader/lib??vue-loader-options!./tab.vue?vue&type=script&lang=js& */ \"./node_modules/vue-loader/lib/index.js?!./assets/vue/tab.vue?vue&type=script&lang=js&\");\n /* harmony default export */ __webpack_exports__[\"a\"] = (_node_modules_vue_loader_lib_index_js_vue_loader_options_tab_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__[/* default */ \"a\"]); \n\n//# sourceURL=webpack:///./assets/vue/tab.vue?");
/***/ }),
/***/ "./assets/vue/tab.vue?vue&type=template&id=65401e0e&":
/*!***********************************************************!*\
!*** ./assets/vue/tab.vue?vue&type=template&id=65401e0e& ***!
\***********************************************************/
/*! exports provided: render, staticRenderFns */
/*! exports used: render, staticRenderFns */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("/* harmony import */ var _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_tab_vue_vue_type_template_id_65401e0e___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!../../node_modules/vue-loader/lib??vue-loader-options!./tab.vue?vue&type=template&id=65401e0e& */ \"./node_modules/vue-loader/lib/loaders/templateLoader.js?!./node_modules/vue-loader/lib/index.js?!./assets/vue/tab.vue?vue&type=template&id=65401e0e&\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"a\", function() { return _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_tab_vue_vue_type_template_id_65401e0e___WEBPACK_IMPORTED_MODULE_0__[\"a\"]; });\n\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"b\", function() { return _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_tab_vue_vue_type_template_id_65401e0e___WEBPACK_IMPORTED_MODULE_0__[\"b\"]; });\n\n\n\n//# sourceURL=webpack:///./assets/vue/tab.vue?");
/***/ }),
/***/ "./assets/vue/tabs.vue":
/*!*****************************!*\
!*** ./assets/vue/tabs.vue ***!
\*****************************/
/*! exports provided: default */
/*! exports used: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("/* harmony import */ var _tabs_vue_vue_type_template_id_466f44d5___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./tabs.vue?vue&type=template&id=466f44d5& */ \"./assets/vue/tabs.vue?vue&type=template&id=466f44d5&\");\n/* harmony import */ var _tabs_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./tabs.vue?vue&type=script&lang=js& */ \"./assets/vue/tabs.vue?vue&type=script&lang=js&\");\n/* harmony import */ var _node_modules_vue_loader_lib_runtime_componentNormalizer_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../node_modules/vue-loader/lib/runtime/componentNormalizer.js */ \"./node_modules/vue-loader/lib/runtime/componentNormalizer.js\");\n\n\n\n\n\n/* normalize component */\n\nvar component = Object(_node_modules_vue_loader_lib_runtime_componentNormalizer_js__WEBPACK_IMPORTED_MODULE_2__[/* default */ \"a\"])(\n _tabs_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__[/* default */ \"a\"],\n _tabs_vue_vue_type_template_id_466f44d5___WEBPACK_IMPORTED_MODULE_0__[/* render */ \"a\"],\n _tabs_vue_vue_type_template_id_466f44d5___WEBPACK_IMPORTED_MODULE_0__[/* staticRenderFns */ \"b\"],\n false,\n null,\n null,\n null\n \n)\n\n/* hot reload */\nif (false) { var api; }\ncomponent.options.__file = \"assets/vue/tabs.vue\"\n/* harmony default export */ __webpack_exports__[\"a\"] = (component.exports);\n\n//# sourceURL=webpack:///./assets/vue/tabs.vue?");
/***/ }),
/***/ "./assets/vue/tabs.vue?vue&type=script&lang=js&":
/*!******************************************************!*\
!*** ./assets/vue/tabs.vue?vue&type=script&lang=js& ***!
\******************************************************/
/*! exports provided: default */
/*! exports used: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("/* harmony import */ var _node_modules_vue_loader_lib_index_js_vue_loader_options_tabs_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../node_modules/vue-loader/lib??vue-loader-options!./tabs.vue?vue&type=script&lang=js& */ \"./node_modules/vue-loader/lib/index.js?!./assets/vue/tabs.vue?vue&type=script&lang=js&\");\n /* harmony default export */ __webpack_exports__[\"a\"] = (_node_modules_vue_loader_lib_index_js_vue_loader_options_tabs_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__[/* default */ \"a\"]); \n\n//# sourceURL=webpack:///./assets/vue/tabs.vue?");
/***/ }),
/***/ "./assets/vue/tabs.vue?vue&type=template&id=466f44d5&":
/*!************************************************************!*\
!*** ./assets/vue/tabs.vue?vue&type=template&id=466f44d5& ***!
\************************************************************/
/*! exports provided: render, staticRenderFns */
/*! exports used: render, staticRenderFns */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("/* harmony import */ var _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_tabs_vue_vue_type_template_id_466f44d5___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!../../node_modules/vue-loader/lib??vue-loader-options!./tabs.vue?vue&type=template&id=466f44d5& */ \"./node_modules/vue-loader/lib/loaders/templateLoader.js?!./node_modules/vue-loader/lib/index.js?!./assets/vue/tabs.vue?vue&type=template&id=466f44d5&\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"a\", function() { return _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_tabs_vue_vue_type_template_id_466f44d5___WEBPACK_IMPORTED_MODULE_0__[\"a\"]; });\n\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"b\", function() { return _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_tabs_vue_vue_type_template_id_466f44d5___WEBPACK_IMPORTED_MODULE_0__[\"b\"]; });\n\n\n\n//# sourceURL=webpack:///./assets/vue/tabs.vue?");
/***/ }),
/***/ "./node_modules/vue-loader/lib/index.js?!./assets/vue/tab.vue?vue&type=script&lang=js&":
/*!*******************************************************************************************************!*\
!*** ./node_modules/vue-loader/lib??vue-loader-options!./assets/vue/tab.vue?vue&type=script&lang=js& ***!
\*******************************************************************************************************/
/*! exports provided: default */
/*! exports used: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("//\n//\n//\n//\n//\n//\n//\n\n/* harmony default export */ __webpack_exports__[\"a\"] = ({\n props: {\n value: { default: undefined },\n },\n\n methods: {\n select() {\n this.$parent.selectTab(this);\n },\n\n onclick(event) {\n this.select();\n /*if(event.target.href != document.location)\n window.history.pushState(\n { url: event.target.href },\n event.target.innerText + ' - ' + document.title,\n event.target.href\n ) */\n }\n }\n});\n\n\n//# sourceURL=webpack:///./assets/vue/tab.vue?./node_modules/vue-loader/lib??vue-loader-options");
/***/ }),
/***/ "./node_modules/vue-loader/lib/index.js?!./assets/vue/tabs.vue?vue&type=script&lang=js&":
/*!********************************************************************************************************!*\
!*** ./node_modules/vue-loader/lib??vue-loader-options!./assets/vue/tabs.vue?vue&type=script&lang=js& ***!
\********************************************************************************************************/
/*! exports provided: default */
/*! exports used: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n\n/* harmony default export */ __webpack_exports__[\"a\"] = ({\n props: {\n default: { default: null },\n },\n\n data() {\n return {\n value: this.default,\n }\n },\n\n computed: {\n tab() {\n const vnode = this.$slots.default && this.$slots.default.find(\n elm => elm.child && elm.child.value == this.value\n );\n return vnode && vnode.child;\n }\n },\n\n methods: {\n selectTab(tab) {\n const value = tab.value;\n if(this.value === value)\n return;\n\n this.value = value;\n this.$emit('select', {target: this, value: value, tab: tab});\n },\n },\n});\n\n\n//# sourceURL=webpack:///./assets/vue/tabs.vue?./node_modules/vue-loader/lib??vue-loader-options");
/***/ }),
/***/ "./node_modules/vue-loader/lib/loaders/templateLoader.js?!./node_modules/vue-loader/lib/index.js?!./assets/vue/tab.vue?vue&type=template&id=65401e0e&":
/*!*****************************************************************************************************************************************************************************************!*\
!*** ./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options!./assets/vue/tab.vue?vue&type=template&id=65401e0e& ***!
\*****************************************************************************************************************************************************************************************/
/*! exports provided: render, staticRenderFns */
/*! exports used: render, staticRenderFns */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"a\", function() { return render; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"b\", function() { return staticRenderFns; });\nvar render = function() {\n var _vm = this\n var _h = _vm.$createElement\n var _c = _vm._self._c || _h\n return _c(\n \"li\",\n {\n class: { \"is-active\": _vm.$parent.value == _vm.value },\n on: {\n click: function($event) {\n $event.preventDefault()\n return _vm.onclick($event)\n }\n }\n },\n [_vm._t(\"default\")],\n 2\n )\n}\nvar staticRenderFns = []\nrender._withStripped = true\n\n\n\n//# sourceURL=webpack:///./assets/vue/tab.vue?./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options");
/***/ }),
/***/ "./node_modules/vue-loader/lib/loaders/templateLoader.js?!./node_modules/vue-loader/lib/index.js?!./assets/vue/tabs.vue?vue&type=template&id=466f44d5&":
/*!******************************************************************************************************************************************************************************************!*\
!*** ./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options!./assets/vue/tabs.vue?vue&type=template&id=466f44d5& ***!
\******************************************************************************************************************************************************************************************/
/*! exports provided: render, staticRenderFns */
/*! exports used: render, staticRenderFns */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"a\", function() { return render; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"b\", function() { return staticRenderFns; });\nvar render = function() {\n var _vm = this\n var _h = _vm.$createElement\n var _c = _vm._self._c || _h\n return _c(\n \"div\",\n [\n _c(\"div\", { staticClass: \"tabs is-centered\" }, [\n _c(\"ul\", [_vm._t(\"tabs\", null, { value: _vm.value })], 2)\n ]),\n _vm._v(\" \"),\n _vm._t(\"default\", null, { value: _vm.value })\n ],\n 2\n )\n}\nvar staticRenderFns = []\nrender._withStripped = true\n\n\n\n//# sourceURL=webpack:///./assets/vue/tabs.vue?./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options");
/***/ })
/******/ });

File diff suppressed because one or more lines are too long

View File

@ -1,79 +0,0 @@
{% load static i18n thumbnail %}
{% comment %}
Context:
- site: current website
{% endcomment %}
<html>
<head>
<meta charset="utf-8">
<meta name="application-name" content="aircox">
<meta name="description" content="{{ site.description }}">
<meta name="keywords" content="{{ site.tags }}">
<link rel="icon" href="{% thumbnail site.favicon 32x32 crop %}" />
{% 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 head_title %}{{ site.title }}{% endblock %}
</title>
{% block head_extra %}{% 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 is-desktop">
<main class="column page">
<header class="header">
{% block header %}
<h1 class="title is-1">{% block title %}{% endblock %}</h1>
{% if parent %}
<h4 class="subtitle is-size-3">
<a href="{{ parent.path }}">
&#10092; {{ parent.title }}</a></li>
</h4>
{% endif %}
{% endblock %}
</header>
{% block main %}{% endblock main %}
</main>
{% if nav_side %}
<aside class="column is-one-third-desktop">
{% block cover %}
{% if cover is not None %}
<img class="cover" src="{{ cover.url }}" class="cover"/>
{% endif %}
{% endblock %}
{% block side_nav %}
{% endblock %}
</aside>
{% endif %}
</div>
</div>
</div>
</body>
</html>

View File

@ -1,66 +0,0 @@
{% load i18n easy_thumbnails_tags aircox_web %}
{% comment %}
Context variables:
- object: the actual diffusion
- page: current parent page in which item is rendered
- hide_schedule: if True, do not display start time
- hide_headline: if True, do not display headline
{% endcomment %}
{% with object.initial|default:object as initial %}
{% with initial.program as program %}
{% with initial.page as d_page %}
{% with program.page as p_page %}
{% with d_page|default:p_page as c_page %}
<article class="media">
<div class="media-left">
<img src="{% thumbnail c_page.cover|default:site.logo 128x128 crop=scale %}"
class="small-cover">
</div>
<div class="media-content">
<h5 class="subtitle is-size-5">
{% if d_page %}
<a href="{{ d_page.path }}">{{ d_page.title }}</a>
{% endif %}
</h5>
<div class="">
{% if not page or p_page != page %}
{% if p_page %}
<a href="{{ p_page.path }}" class="has-text-grey-dark">
{{ p_page.title }}</a>
{% else %}
{{ program.name }}
{% endif %}
{% if not hide_schedule %} &mdash; {% endif %}
{% endif %}
{% if not hide_schedule %}
<time datetime="{{ object.start|date:"c" }}" title="{{ object.start }}"
class="has-text-weight-light is-size-6">
{{ object.start|date:"d M, H:i" }}
</time>
{% endif %}
{% if object.initial %}
{% with object.initial.date as date %}
<span class="tag is-info" title="{% blocktrans %}Rerun of {{ date }}{% endblocktrans %}">
{% trans "rerun" %}
</span>
{% endwith %}
{% endif %}
</span>
</div>
{% if not hide_headline %}
<div class="content">
{{ c_page.headline }}
</div>
{% endif %}
</div>
</article>
{% endwith %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endwith %}

View File

@ -1,14 +0,0 @@
{% extends "aircox_web/program_base.html" %}
{% load i18n %}
{% block main %}
{{ block.super }}
{% if podcasts %}
{% for object in podcasts %}
{% include "aircox_web/podcast_item.html" %}
{% endfor %}
{% endif %}
{% endwith %}

View File

@ -1,51 +0,0 @@
{% extends "aircox_web/page.html" %}
{% load i18n aircox_web %}
{% block title %}
{% if program %}
{% with program.name as program %}
{% blocktrans %}Diffusions of {{ program }}{% endblocktrans %}
{% endwith %}
{% else %}
{% trans "All diffusions" %}
{% endif %}
{% endblock %}
{% block content %}
<section>
{% for object in object_list %}
{% include "aircox_web/diffusion_item.html" %}
{% endfor %}
</section>
{% if is_paginated %}
<nav class="pagination is-centered" role="pagination" aria-label="{% trans "pagination" %}">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}" class="pagination-previous">
{% else %}
<a class="pagination-previous" disabled>
{% endif %}
{% trans "Previous" %}</a>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}" class="pagination-next">
{% else %}
<a class="pagination-next" disabled>
{% endif %}
{% trans "Next" %}</a>
<ul class="pagination-list">
{% for i in paginator.page_range %}
<li>
<a class="pagination-link {% if page_obj.number == i %}is-current{% endif %}"
href="?page={{ i }}">{{ i }}</a>
</li>
{% endfor %}
</ul>
</nav>
{% endif %}
</section>
{% endblock %}

View File

@ -1,19 +0,0 @@
{% load i18n %}
{% with object.track as track %}
<span class="has-text-info is-size-5">&#9836;</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 %}

View File

@ -1,53 +0,0 @@
{% 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> #}
{% with True as hide_schedule %}
<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>
{% endwith %}
</section>
{% endblock %}

View File

@ -1,30 +0,0 @@
{% extends "aircox_web/base.html" %}
{% load static i18n thumbnail %}
{% comment %}
Context:
- cover: cover image
- title: title
- page: page
{% endcomment %}
{% block head_title %}
{% block title %}{{ title }}{% endblock %}
{% if title %} &mdash; {% endif %}
{{ site.title }}
{% endblock %}
{% block main %}
{% block headline %}
{% if page and page.headline %}
<p class="headline">{{ page.headline }}</p>
{% endif %}
{% endblock %}
{% block content %}
{{ regions.content }}
{% endblock %}
{% endblock %}

View File

@ -1,8 +0,0 @@
<div class="podcast">
{% if object.embed %}
{{ object.embed }}
{% else %}
<audio src="{{ object.url }}" controls>
{% endif %}
</div>

View File

@ -1,29 +0,0 @@
{% extends "aircox_web/page.html" %}
{% load i18n %}
{% block side_nav %}
{{ block.super }}
{% if diffusions %}
<section>
<h4 class="subtitle is-size-4">{% trans "Last shows" %}</h4>
{% for object in diffusions %}
{% include "aircox_web/diffusion_item.html" %}
{% endfor %}
<br>
<nav class="pagination is-centered">
<ul class="pagination-list">
<li>
<a href="{% url "diffusion-list" program_slug=page.slug %}"
class="pagination-link"
aria-label="{% trans "Show all diffusions" %}">
{% trans "All diffusions" %}
</a>
</li>
</ul>
</section>
{% endif %}
{% endblock %}

View File

@ -1,25 +0,0 @@
{% load i18n %}
<section class="is-size-5">
{% for schedule in program.schedule_set.all %}
<p>
{{ schedule.get_frequency_verbose }}
{% with schedule.start|date:"H:i" as start %}
{% with schedule.end|date:"H:i" as end %}
<time datetime="{{ start }}">{{ start }}</time>
&mdash;
<time datetime="{{ end }}">{{ end }}</time>
{% endwith %}
{% endwith %}
<small>
{% if schedule.initial %}
{% with schedule.initial.date as date %}
<span title="{% blocktrans %}Rerun of {{ date }}{% endblocktrans %}">
({% trans "rerun" %})
</span>
{% endwith %}
{% endif %}
</small>
</p>
{% endfor %}
</section>

View File

@ -1,8 +0,0 @@
{% extends "aircox_web/program_base.html" %}
{% load i18n %}
{% block header %}
{{ block.super }}
{% include "aircox_web/program_header.html" %}
{% endblock %}

View File

@ -1,58 +0,0 @@
{% extends "aircox_web/page.html" %}
{% load i18n aircox_web %}
{% block title %}
{% trans "Timetable" %}
{% endblock %}
{% 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 %}">&lt;</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 %}">&gt;</a>
</li>
</template>
{% with True as hide_schedule %}
<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>
{% endwith %}
</a-tabs>
</section>
{% endblock %}

View File

@ -1,32 +0,0 @@
import random
from django import template
from aircox.models import Page,
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. """
episode = diffusion.episode
if episode.is_publihed:
return diff.episode
program = episode.program
return program if program.is_published else None
@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)

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@ -1,29 +0,0 @@
from django.conf.urls import url
from django.urls import path, register_converter
from . import views, models
from .converters import PagePathConverter, DateConverter, WeekConverter
register_converter(PagePathConverter, 'page_path')
register_converter(DateConverter, 'date')
register_converter(WeekConverter, 'week')
urlpatterns = [
path('programs/<slug:slug>/',
views.ProgramPageView.as_view(), name='program-page'),
path('diffusion/<slug:slug>/',
views.DiffusionPageView.as_view(), name='diffusion-page'),
path('programs/<slug:program_slug>/diffusions/',
views.DiffusionsView.as_view(), name='diffusion-list'),
path('diffusions/',
views.TimetableView.as_view(), name='timetable'),
path('diffusions/<week:date>/',
views.TimetableView.as_view(), name='timetable'),
path('diffusions/all',
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'),
]

View File

@ -1,296 +0,0 @@
from collections import OrderedDict, deque
import datetime
from django.http import Http404
from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext_lazy as _
from django.views.generic import DetailView, 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, DiffusionPage, ProgramPage, \
get_diffusions_with_page
from .renderer import site_renderer, page_renderer
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(
model.objects.select_subclasses().live(),
path=path
)
kwargs['page'] = page
return page.view(request, *args, site=site, **kwargs)
class BaseView(TemplateResponseMixin, ContextMixin):
site = None
""" Current website """
nav_side = False
""" Show side navigation """
title = None
""" Page title """
cover = None
""" Page cover """
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)
kwargs.setdefault('cover', self.cover)
kwargs.setdefault('nav_side', self.nav_side)
return super().get_context_data(**kwargs)
class PageView(BaseView, DetailView):
""" Base view class for pages. """
template_name = 'aircox_web/page.html'
context_object_name = 'page'
page = None
def get_queryset(self):
return super().get_queryset().live()
def get_context_data(self, **kwargs):
page = getattr(self, 'object', None)
if page is not None:
if kwargs.get('regions') is None:
contents = contents_for_item(
page, page_renderer._renderers.keys())
kwargs['regions'] = contents.render_regions(page_renderer)
kwargs.setdefault('title', page.title)
kwargs.setdefault('cover', page.cover)
kwargs.setdefault('page', page)
return super().get_context_data(**kwargs)
class BaseProgramView(PageView):
""" Base view class for programs and their sub-pages. """
nav_side = True
list_count=5
def get_diffusions_queryset(self, program, queryset=None):
qs = get_diffusions_with_page() if queryset is None else queryset
return qs.before().filter(program=program).order_by('-start')
def get_context_data(self, program, **kwargs):
if not hasattr(program, 'page') or not program.page.is_published:
raise Http404
if 'diffusions' not in kwargs:
diffs = self.get_diffusions_queryset(program)[:self.list_count]
kwargs['diffusions'] = diffs
return super().get_context_data(program=program, **kwargs)
class ProgramPageView(BaseProgramView):
template_name = 'aircox_web/program_page.html'
model = ProgramPage
def get_queryset(self):
return super().get_queryset().select_related('program')
def get_context_data(self, **kwargs):
kwargs.setdefault('program', self.object.program)
return super().get_context_data(**kwargs)
class DiffusionPageView(BaseProgramView):
template_name = 'aircox_web/program_base.html'
model = DiffusionPage
def get_podcasts(self, diffusion):
return aircox.Sound.objects.diffusion(diffusion).podcasts()
def get_context_data(self, **kwargs):
diffusion = self.object.diffusion
kwargs.setdefault('program', diffusion.program)
kwargs.setdefault('parent', getattr(kwargs['program'], 'page', None))
if not 'podcasts' in kwargs:
kwargs['podcasts'] = self.get_podcasts(diffusion)
print('get prodcasts...', kwargs['podcasts'], diffusion)
return super().get_context_data(**kwargs)
# TODO: pagination: in template, only a limited number of pages displayed
# DiffusionsView use diffusion instead of diffusion page for different reasons:
# more straightforward, it handles reruns
class DiffusionsView(BaseView, ListView):
template_name = 'aircox_web/diffusions.html'
model = aircox.Diffusion
paginate_by = 30
program = None
def get(self, request, *args, **kwargs):
program_slug = kwargs.get('program_slug')
if program_slug:
self.program = get_object_or_404(
aircox.Program, slug=kwargs.get('program_slug'))
return super().get(request, *args, **kwargs)
def get_queryset(self):
qs = get_diffusions_with_page(super().get_queryset()) \
.select_related('page', 'program')
if self.program:
qs = qs.filter(program=self.program)
return qs.order_by('-start')
def get_context_data(self, **kwargs):
program = kwargs.setdefault('program', self.program)
if program is not None and hasattr(program, 'page'):
kwargs.setdefault('cover', program.page.cover)
kwargs.setdefault('parent', program.page)
return super().get_context_data(**kwargs)
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') or datetime.date.today()
self.start = self.date - datetime.timedelta(days=self.date.weekday())
self.end = self.start + datetime.timedelta(days=7)
return super().get_queryset().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
)

View File

@ -1,93 +0,0 @@
const path = require('path');
const webpack = require('webpack');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
// const { createLodashAliases } = require('lodash-loader');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
module.exports = (env, argv) => Object({
context: __dirname,
entry: './assets/index',
output: {
path: path.resolve('static/aircox_web/assets'),
filename: '[name].js',
chunkFilename: '[name].js',
},
optimization: {
usedExports: true,
concatenateModules: argv.mode == 'production' ? true : false,
splitChunks: {
cacheGroups: {
vendor: {
name: 'vendor',
chunks: 'initial',
enforce: true,
test: /[\\/]node_modules[\\/]/,
},
/*noscript: {
name: 'noscript',
chunks: 'initial',
enforce: true,
test: /noscript/,
}*/
}
}
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].css",
chunkFilename: "[id].css"
}),
new VueLoaderPlugin(),
],
module: {
rules: [
{ test: /\.vue$/, loader: 'vue-loader' },
{
test: /\/node_modules\//,
sideEffects: false
},
{
test: /\.scss$/,
use: [ { loader: MiniCssExtractPlugin.loader },
{ loader: 'css-loader' },
{ loader: 'sass-loader' , options: { sourceMap: true }} ],
},
{
// TODO: remove ttf eot svg
test: /\.(ttf|eot|svg|woff2?)$/,
use: [{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'fonts/',
}
}],
},
],
},
resolve: {
alias: {
js: path.resolve(__dirname, 'assets/js'),
vue: 'vue/dist/vue.esm.browser.js',
// buefy: 'buefy/dist/buefy.js',
},
modules: [
'assets/css',
'assets/js',
'assets/vue',
'./node_modules',
],
extensions: ['.js', '.vue', '.css', '.styl', '.ttf']
},
})

View File

@ -14,6 +14,9 @@ $body-background-color: $light;
} }
.is-borderless { border: none; } .is-borderless { border: none; }
.has-background-transparent {
background-color: transparent;
}
.navbar + .container { .navbar + .container {
margin-top: 1em; margin-top: 1em;
@ -23,6 +26,10 @@ $body-background-color: $light;
box-shadow: 0em 0em 1em rgba(0,0,0,0.1); box-shadow: 0em 0em 1em rgba(0,0,0,0.1);
} }
a.navbar-item.is-active {
border-bottom: 1px grey solid;
}
/* /*
.navbar-brand img { .navbar-brand img {
min-height: 6em; min-height: 6em;

View File

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<div class="tabs is-centered"> <div class="tabs is-centered is-medium">
<ul><slot name="tabs" :value="value" /></ul> <ul><slot name="tabs" :value="value" /></ul>
</div> </div>

View File

@ -47,13 +47,12 @@ MEDIA_ROOT = os.path.join(STATIC_ROOT, 'media')
######################################################################## ########################################################################
# set current language code. e.g. 'fr_BE' # set current language code. e.g. 'fr_BE'
LANGUAGE_CODE = os.environ.get('LANG') or 'en_US' LANGUAGE_CODE = 'en_US'
# locale
LC_LOCALE = 'en_US.UTF-8'
# set current timezone. e.g. 'Europe/Brussels' # set current timezone. e.g. 'Europe/Brussels'
TIME_ZONE = os.environ.get('TZ') or 'UTC' TIME_ZONE = os.environ.get('TZ') or 'UTC'
# wagtail site name
WAGTAIL_SITE_NAME = 'Aircox'
# debug mode # debug mode
DEBUG = (os.environ['AIRCOX_DEBUG'].lower() in ('true', 1)) \ DEBUG = (os.environ['AIRCOX_DEBUG'].lower() in ('true', 1)) \
if 'AIRCOX_DEBUG' in os.environ else \ if 'AIRCOX_DEBUG' in os.environ else \
@ -64,7 +63,7 @@ if DEBUG:
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 'NAME': os.path.join(PROJECT_ROOT, 'db.sqlite3'),
'TIMEZONE': TIME_ZONE, 'TIMEZONE': TIME_ZONE,
} }
} }
@ -80,6 +79,13 @@ else:
'TIMEZONE': TIME_ZONE, 'TIMEZONE': TIME_ZONE,
}, },
} }
# caching uses memcache
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': '127.0.0.1:11211',
}
}
# allowed hosts # allowed hosts
ALLOWED_HOSTS = ('127.0.0.1',) ALLOWED_HOSTS = ('127.0.0.1',)
@ -102,7 +108,7 @@ timezone.activate(pytz.timezone(TIME_ZONE))
try: try:
import locale import locale
locale.setlocale(locale.LC_ALL, LANGUAGE_CODE) locale.setlocale(locale.LC_ALL, LC_LOCALE)
except: except:
print( print(
'Can not set locale {LC}. Is it available on you system? Hint: ' 'Can not set locale {LC}. Is it available on you system? Hint: '
@ -115,28 +121,17 @@ except:
# Application definition # Application definition
INSTALLED_APPS = ( INSTALLED_APPS = (
# aircox & dependencies
'aircox', 'aircox',
'aircox_cms', 'rest_framework',
"ckeditor",
'jet', 'easy_thumbnails',
'wagtail.contrib.forms', 'filer',
'wagtail.contrib.redirects',
'wagtail.embeds',
'wagtail.sites',
'wagtail.users',
'wagtail.snippets',
'wagtail.documents',
'wagtail.images',
'wagtail.search',
'wagtail.admin',
'wagtail.core',
'wagtail.contrib.settings',
'wagtail.contrib.modeladmin',
'modelcluster',
'taggit', 'taggit',
'adminsortable2',
'honeypot', 'honeypot',
# django
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.sessions', 'django.contrib.sessions',
@ -147,7 +142,6 @@ INSTALLED_APPS = (
MIDDLEWARE = ( MIDDLEWARE = (
'django.middleware.gzip.GZipMiddleware', 'django.middleware.gzip.GZipMiddleware',
'htmlmin.middleware.HtmlMinifyMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
@ -156,9 +150,6 @@ MIDDLEWARE = (
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'wagtail.core.middleware.SiteMiddleware',
'wagtail.contrib.redirects.middleware.RedirectMiddleware',
'aircox.middleware.AircoxMiddleware' 'aircox.middleware.AircoxMiddleware'
) )
@ -180,12 +171,7 @@ TEMPLATES = [
"django.template.context_processors.static", "django.template.context_processors.static",
"django.template.context_processors.tz", "django.template.context_processors.tz",
"django.contrib.messages.context_processors.messages", "django.contrib.messages.context_processors.messages",
'wagtail.contrib.settings.context_processors.settings',
), ),
'builtins': [
'overextends.templatetags.overextends_tags'
],
'loaders': ( 'loaders': (
'django.template.loaders.filesystem.Loader', 'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader', 'django.template.loaders.app_directories.Loader',
@ -197,6 +183,7 @@ TEMPLATES = [
WSGI_APPLICATION = 'instance.wsgi.application' WSGI_APPLICATION = 'instance.wsgi.application'
# FIXME: what about dev & prod modules?
LOGGING = { LOGGING = {
'version': 1, 'version': 1,
'disable_existing_loggers': False, 'disable_existing_loggers': False,
@ -206,7 +193,11 @@ LOGGING = {
}, },
}, },
'loggers': { 'loggers': {
'aircox.core': { 'aircox': {
'handlers': ['console'],
'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'),
},
'aircox.commands': {
'handlers': ['console'], 'handlers': ['console'],
'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'), 'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'),
}, },
@ -214,10 +205,6 @@ LOGGING = {
'handlers': ['console'], 'handlers': ['console'],
'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'), 'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'),
}, },
'aircox.tools': {
'handlers': ['console'],
'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'),
},
}, },
} }