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
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
# Aircox
Aircox application aims to provide basis of a radio management system.
## 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:
* **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;
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.
Each program has a directory in **AIRCOX_PROGRAMS_DIR**; For each, subdir:
* **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
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).
## manage.py's commands

View File

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

View File

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

View File

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

View File

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

View File

@ -152,7 +152,7 @@ class Monitor:
.now(air_time).first()
# 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,
comment=air_uri)
@ -177,7 +177,7 @@ class Monitor:
if pos > now:
break
# 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)
def handle_diffusions(self):
@ -208,7 +208,7 @@ class Monitor:
#
now = tz.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()
# Can't use delay: diffusion may start later than its assigned start.
log = None if not diff else self.logs.start().filter(diffusion=diff)
@ -228,13 +228,13 @@ class Monitor:
def start_diff(self, source, diff):
playlist = Sound.objects.episode(id=diff.episode_id).paths()
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))
def cancel_diff(self, source, diff):
diff.type = Diffusion.Type.cancel
diff.type = Diffusion.TYPE_CANCEL
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))
def sync(self):

View File

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

View File

@ -1,9 +1,7 @@
import datetime
from enum import IntEnum
from django.db import models
from django.db.models import F, Q
from django.db.models.functions import Concat, Substr
from django.db.models import Q
from django.utils import timezone as tz
from django.utils.translation import ugettext_lazy as _
from django.utils.functional import cached_property
@ -64,7 +62,7 @@ class DiffusionQuerySet(BaseRerunQuerySet):
def on_air(self):
""" 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):
""" Diffusions occuring now """
@ -132,20 +130,20 @@ class Diffusion(BaseRerun):
"""
objects = DiffusionQuerySet.as_manager()
class Type(IntEnum):
on_air = 0x00
unconfirmed = 0x01
cancel = 0x02
TYPE_ON_AIR = 0x00
TYPE_UNCONFIRMED = 0x01
TYPE_CANCEL = 0x02
TYPE_CHOICES = (
(TYPE_ON_AIR, _('on air')),
(TYPE_UNCONFIRMED, _('not confirmed')),
(TYPE_CANCEL, _('cancelled')),
)
episode = models.ForeignKey(
Episode, models.CASCADE,
verbose_name=_('episode'),
Episode, models.CASCADE, verbose_name=_('episode'),
)
type = models.SmallIntegerField(
verbose_name=_('type'),
default=Type.on_air,
choices=[(int(y), _(x.replace('_', ' ')))
for x, y in Type.__members__.items()],
verbose_name=_('type'), default=TYPE_ON_AIR, choices=TYPE_CHOICES,
)
start = models.DateTimeField(_('start'))
end = models.DateTimeField(_('end'))
@ -222,7 +220,7 @@ class Diffusion(BaseRerun):
# TODO: property?
def is_live(self):
""" 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()
def get_playlist(self, **types):
@ -232,7 +230,7 @@ class Diffusion(BaseRerun):
"""
from .sound import Sound
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))
def get_sounds(self, **types):

View File

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

View File

@ -38,28 +38,30 @@ class Category(models.Model):
class PageQuerySet(InheritanceQuerySet):
def draft(self):
return self.filter(status=Page.STATUS.draft)
return self.filter(status=Page.STATUS_DRAFT)
def published(self):
return self.filter(status=Page.STATUS.published)
return self.filter(status=Page.STATUS_PUBLISHED)
def trash(self):
return self.filter(status=Page.STATUS.trash)
return self.filter(status=Page.STATUS_TRASH)
class Page(models.Model):
""" Base class for publishable content """
class STATUS(IntEnum):
draft = 0x00
published = 0x10
trash = 0x20
STATUS_DRAFT = 0x00
STATUS_PUBLISHED = 0x10
STATUS_TRASH = 0x20
STATUS_CHOICES = (
(STATUS_DRAFT, _('draft')),
(STATUS_PUBLISHED, _('published')),
(STATUS_TRASH, _('trash')),
)
title = models.CharField(max_length=128)
slug = models.SlugField(_('slug'), blank=True, unique=True)
status = models.PositiveSmallIntegerField(
_('status'),
default=STATUS.draft,
choices=[(int(y), _(x)) for x, y in STATUS.__members__.items()],
_('status'), default=STATUS_DRAFT, choices=STATUS_CHOICES,
)
category = models.ForeignKey(
Category, models.SET_NULL,
@ -84,8 +86,6 @@ class Page(models.Model):
detail_url_name = None
class Meta:
abstract = True
def __str__(self):
return '{}: {}'.format(self._meta.verbose_name,
@ -104,15 +104,15 @@ class Page(models.Model):
@property
def is_draft(self):
return self.status == self.STATUS.draft
return self.status == self.STATUS_DRAFT
@property
def is_published(self):
return self.status == self.STATUS.published
return self.status == self.STATUS_PUBLISHED
@property
def is_trash(self):
return self.status == self.STATUS.trash
return self.status == self.STATUS_TRASH
@cached_property
def headline(self):
@ -132,6 +132,16 @@ class Page(models.Model):
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):
""" Navigation menu items """
station = models.ForeignKey(

View File

@ -45,6 +45,11 @@ class Program(Page):
Renaming a Program rename the corresponding directory to matches the new
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,
verbose_name=_('station'),
@ -478,7 +483,7 @@ class Schedule(BaseRerun):
initial = diffusions[initial]
diffusions[date] = Diffusion(
episode=episode, type=Diffusion.Type.on_air,
episode=episode, type=Diffusion.TYPE_ON_AIR,
initial=initial, start=date, end=date+duration
)
return episodes.values(), diffusions.values()

View File

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

View File

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

View File

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

View File

@ -419,7 +419,7 @@ eval("/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__,
/***/ (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");
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 %}
</main>
{% if show_side_nav %}
<aside class="column is-one-third-desktop">
{% block cover %}

View File

@ -1,20 +1,28 @@
{% extends "aircox/page.html" %}
{% 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.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 %}
<a-tabs default="{{ date }}">
<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 %}
<a-tab value="{{ day }}">
@ -25,11 +33,10 @@
{% endfor %}
<li>
<a href="{% url "timetable" date=next_date %}">&gt;</a>
<a href="{% url "timetable" date=next_date %}">{% trans "After" %} &#10093;</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>
@ -51,8 +58,8 @@
</div>
{% endfor %}
</template>
{% endwith %}
</a-tabs>
</section>
{% endwith %}
{% endblock %}

View File

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

View File

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

View File

@ -11,7 +11,7 @@
<section class="section">
{% 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>
{% for day in dates %}
<li {% if day == date %}class="is-active"{% endif %}>
@ -30,7 +30,7 @@
{# <h4 class="subtitle size-4">{{ date }}</h4> #}
{% 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 %}
<tr>
<td>

View File

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

View File

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

View File

@ -17,7 +17,7 @@
</noscript>
<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" %}">
<template v-slot:sources>
{% for stream in audio_streams %}

View File

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

View File

@ -6,3 +6,39 @@
{% include "aircox/program_header.html" %}
{% 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),
name='article-list'),
path(_('articles/<slug:slug>/'),
views.PageDetailView.as_view(model=models.Article),
views.ArticleDetailView.as_view(),
name='article-detail'),
path(_('programs/'), views.PageListView.as_view(model=models.Program),
name='program-list'),
path(_('programs/<slug:slug>/'),
views.ProgramDetailView.as_view(), name='program-detail'),
path(_('programs/<slug:program_slug>/episodes/'),
path(_('programs/<slug:parent_slug>/episodes/'),
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'),
path(_('episodes/'),
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>/'),
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/<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 .article import ArticleListView
from .article import ArticleDetailView, ArticleListView
from .base import BaseView
from .episode import EpisodeDetailView, EpisodeListView, TimetableView
from .log import LogListView

View File

@ -1,16 +1,36 @@
from ..models import Article
from .program import ProgramPageListView
from ..models import Article, Program
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
template_name = 'aircox/article_list.html'
show_headline = True
is_static = False
def get_queryset(self):
return super().get_queryset(is_static=self.is_static)
parent_model = Program
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
__all__ = ['BaseView', 'PageView']
__all__ = ['BaseView']
class BaseView(TemplateResponseMixin, ContextMixin):
show_side_nav = False
""" Show side navigation """
title = None
""" Page title """
cover = None
""" Page cover """
show_side_nav = False
""" Show side navigation """
list_count = 5
""" Item count for small lists displayed on page. """
@property
def station(self):
return self.request.station
@ -24,14 +27,24 @@ class BaseView(TemplateResponseMixin, ContextMixin):
def get_queryset(self):
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('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:
streams = self.station.audio_streams
streams = streams and streams.split('\n')
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
import datetime
from django.db.models import OuterRef, Subquery
from django.shortcuts import get_object_or_404
from django.views.generic import ListView
from ..models import Diffusion, Episode, Page, Program, Sound
from ..converters import WeekConverter
from ..models import Diffusion, Episode, Program, Sound
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):
@ -20,8 +20,9 @@ class EpisodeDetailView(ProgramPageDetailView):
return Sound.objects.diffusion(diffusion).podcasts()
def get_context_data(self, **kwargs):
kwargs.setdefault('program', self.object.program)
kwargs.setdefault('parent', kwargs['program'])
self.program = kwargs.setdefault('program', self.object.program)
kwargs.setdefault('parent', self.program)
if not 'tracks' in kwargs:
kwargs['tracks'] = self.object.track_set.order_by('position')
if not 'podcasts' in kwargs:
@ -29,12 +30,15 @@ class EpisodeDetailView(ProgramPageDetailView):
return super().get_context_data(**kwargs)
class EpisodeListView(ProgramPageListView):
class EpisodeListView(ParentMixin, PageListView):
model = Episode
template_name = 'aircox/diffusion_list.html'
item_template_name = 'aircox/episode_item.html'
show_headline = True
parent_model = Program
fk_parent = 'program'
class TimetableView(BaseView, ListView):
""" View for timetables """

View File

@ -1,6 +1,7 @@
from django.core.exceptions import FieldDoesNotExist
from django.http import Http404
from django.shortcuts import get_object_or_404
from django.views.generic import DetailView, ListView
from ..models import Category
@ -8,41 +9,48 @@ from ..utils import Redirect
from .base import BaseView
__all__ = ['PageDetailView', 'PageListView']
__all__ = ['ParentMixin', 'PageDetailView', 'PageListView']
class PageDetailView(BaseView, DetailView):
""" Base view class for pages. """
context_object_name = 'page'
class ParentMixin:
"""
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):
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
if self.parent is not None:
lookup = {self.fk_parent: self.parent}
return super().get_queryset().filter(**lookup)
return super().get_queryset()
def get_context_data(self, **kwargs):
#if kwargs.get('regions') is None:
# contents = contents_for_item(
# page, page_renderer._renderers.keys())
# kwargs['regions'] = contents.render_regions(page_renderer)
page = kwargs.setdefault('page', self.object)
kwargs.setdefault('title', page.title)
kwargs.setdefault('cover', page.cover)
parent = kwargs.setdefault('parent', self.parent)
if parent is not None:
kwargs.setdefault('cover', parent.cover)
return super().get_context_data(**kwargs)
@ -84,4 +92,35 @@ class PageListView(BaseView, ListView):
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.
"""
program = None
show_side_nav = True
list_count = 5
def get_episodes_queryset(self, program):
return program.episode_set.published().order_by('-date')
def get_context_data(self, program, episodes=None, **kwargs):
if episodes is None:
episodes = self.get_episodes_queryset(program)
return super().get_context_data(
program=program, episodes=episodes[:self.list_count], **kwargs)
class 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)
def get_side_queryset(self):
return self.program.episode_set.published().order_by('-date')
class ProgramDetailView(ProgramPageDetailView):
model = Program
def get_articles_queryset(self, program):
return program.article_set.published().order_by('-date')
def get_articles_queryset(self):
return self.program.article_set.published().order_by('-date')
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)

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

View File

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

View File

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