continue migration: make website working

This commit is contained in:
bkfox 2019-08-05 23:45:20 +02:00
parent 432467ec8d
commit 324cf2ef0f
54 changed files with 8658 additions and 115 deletions

View File

@ -1,28 +1,7 @@
from django.contrib import admin
from .episode import DiffusionAdmin, EpisodeAdmin from .episode import DiffusionAdmin, EpisodeAdmin
from .log import LogAdmin
# from .playlist import PlaylistAdmin # from .playlist import PlaylistAdmin
from .program import ProgramAdmin, ScheduleAdmin, StreamAdmin from .program import ProgramAdmin, ScheduleAdmin, StreamAdmin
from .sound import SoundAdmin from .sound import SoundAdmin
from .station import StationAdmin
from aircox.models import Log, Port, Station
class PortInline(admin.StackedInline):
model = Port
extra = 0
@admin.register(Station)
class StationAdmin(admin.ModelAdmin):
prepopulated_fields = {'slug': ('name',)}
inlines = [PortInline]
@admin.register(Log)
class LogAdmin(admin.ModelAdmin):
list_display = ['id', 'date', 'station', 'source', 'type', 'comment']
list_filter = ['date', 'source', 'station']

View File

@ -7,6 +7,7 @@ from aircox.models import Episode, Diffusion, Sound, Track
from .page import PageAdmin from .page import PageAdmin
from .playlist import TracksInline from .playlist import TracksInline
from .sound import SoundInline
class DiffusionBaseAdmin: class DiffusionBaseAdmin:
@ -46,14 +47,6 @@ class DiffusionInline(DiffusionBaseAdmin, admin.TabularInline):
return request.user.has_perm('aircox_program.scheduling') return request.user.has_perm('aircox_program.scheduling')
class SoundInline(admin.TabularInline):
model = Sound
fk_name = 'episode'
fields = ['type', 'path', 'duration', 'is_public']
readonly_fields = ['type']
extra = 0
@admin.register(Episode) @admin.register(Episode)
class EpisodeAdmin(PageAdmin): class EpisodeAdmin(PageAdmin):
list_display = PageAdmin.list_display + ('program',) list_display = PageAdmin.list_display + ('program',)

13
aircox/admin/log.py Normal file
View File

@ -0,0 +1,13 @@
from django.contrib import admin
from ..models import Log
__all__ = ['LogAdmin']
@admin.register(Log)
class LogAdmin(admin.ModelAdmin):
list_display = ['id', 'date', 'station', 'source', 'type', 'comment']
list_filter = ['date', 'source', 'station']

View File

@ -2,6 +2,10 @@ from django.contrib import admin
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from adminsortable2.admin import SortableInlineAdminMixin
from ..models import NavItem
class PageAdmin(admin.ModelAdmin): class PageAdmin(admin.ModelAdmin):
list_display = ('cover_thumb', 'title', 'status') list_display = ('cover_thumb', 'title', 'status')
@ -24,5 +28,6 @@ class PageAdmin(admin.ModelAdmin):
if obj.cover else '' if obj.cover else ''
class NavItemInline(SortableInlineAdminMixin, admin.TabularInline):
model = NavItem

View File

@ -5,6 +5,13 @@ from aircox.models import Sound
from .playlist import TracksInline from .playlist import TracksInline
class SoundInline(admin.TabularInline):
model = Sound
fields = ['type', 'path', 'duration', 'is_public']
readonly_fields = ['type']
extra = 0
@admin.register(Sound) @admin.register(Sound)
class SoundAdmin(admin.ModelAdmin): class SoundAdmin(admin.ModelAdmin):
def filename(self, obj): def filename(self, obj):
@ -24,3 +31,4 @@ class SoundAdmin(admin.ModelAdmin):
inlines = [TracksInline] inlines = [TracksInline]

20
aircox/admin/station.py Normal file
View File

@ -0,0 +1,20 @@
from django.contrib import admin
from ..models import Port, Station
from .page import NavItemInline
__all__ = ['PortInline', 'StationAdmin']
class PortInline(admin.StackedInline):
model = Port
extra = 0
@admin.register(Station)
class StationAdmin(admin.ModelAdmin):
prepopulated_fields = {'slug': ('name',)}
inlines = [PortInline, NavItemInline]

View File

@ -15,6 +15,7 @@ from django.utils import timezone as tz
from . import settings from . import settings
from .models import Port, Station, Sound from .models import Port, Station, Sound
from .connector import Connector from .connector import Connector
from .utils import to_seconds
# FIXME liquidsoap does not manage timezones -- we have to convert # FIXME liquidsoap does not manage timezones -- we have to convert
@ -297,9 +298,6 @@ class PlaylistSource(Source):
if not stream or (not stream.begin and not stream.delay): if not stream or (not stream.begin and not stream.delay):
return return
def to_seconds(time):
return 3600 * time.hour + 60 * time.minute + time.second
return { return {
'begin': stream.begin.strftime('%Hh%M') if stream.begin else None, 'begin': stream.begin.strftime('%Hh%M') if stream.begin else None,
'end': stream.end.strftime('%Hh%M') if stream.end else None, 'end': stream.end.strftime('%Hh%M') if stream.end else None,

View File

@ -12,7 +12,6 @@ used to:
# x cancel # x cancel
# x when liquidsoap fails to start/exists: exit # x when liquidsoap fails to start/exists: exit
# - handle restart after failure # - handle restart after failure
# - file in queue without sound not logged?
# - is stream restart after live ok? # - is stream restart after live ok?
from argparse import RawTextHelpFormatter from argparse import RawTextHelpFormatter
import time import time

View File

@ -1,25 +1,13 @@
import pytz import pytz
import django.utils.timezone as tz from django import shortcuts
from django.db.models import Q, Case, Value, When
from django.utils import timezone as tz
import aircox.models as models from .models import Station
from .utils import Redirect
class AircoxInfo: __all__ = ['AircoxMiddleware']
"""
Used to store informations about Aircox on a request. Some of theses
information are None when user is anonymous.
"""
station = None
"""
Current station
"""
default_station = False
"""
Default station is used as the current station
"""
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
class AircoxMiddleware(object): class AircoxMiddleware(object):
@ -31,37 +19,19 @@ class AircoxMiddleware(object):
def __init__(self, get_response): def __init__(self, get_response):
self.get_response = get_response self.get_response = get_response
def get_station(self, request):
def update_station(self, request): """ Return station for the provided request """
station = request.GET.get('aircox.station') expr = Q(default=True) | Q(hosts__contains=request.get_host())
pk = None #case = Case(When(hosts__contains=request.get_host(), then=Value(0)),
try: # When(default=True, then=Value(32)))
if station is not None: return Station.objects.filter(expr).order_by('default').first()
pk = request.GET['aircox.station'] # .annotate(resolve_priority=case) \
if station: #.order_by('resolve_priority').first()
pk = int(pk)
if models.Station.objects.filter(pk = station).exists():
request.session['aircox.station'] = pk
except:
pass
def init_station(self, request, aircox):
self.update_station(request)
try:
pk = request.session.get('aircox.station')
pk = int(pk) if pk else None
except:
pk = None
aircox.station = models.Station.objects.default(pk)
aircox.default_station = (pk is None)
def init_timezone(self, request, aircox): def init_timezone(self, request):
# note: later we can use http://freegeoip.net/ on user side if # note: later we can use http://freegeoip.net/ on user side if
# required # required
# TODO: add to request's session
timezone = None timezone = None
try: try:
timezone = request.session.get('aircox.timezone') timezone = request.session.get('aircox.timezone')
@ -76,14 +46,11 @@ class AircoxMiddleware(object):
def __call__(self, request): def __call__(self, request):
tz.activate(pytz.timezone('Europe/Brussels')) self.init_timezone(request)
aircox = AircoxInfo() request.station = self.get_station(request)
try:
if request.user.is_authenticated: return self.get_response(request)
self.init_station(request, aircox) except Redirect as redirect:
self.init_timezone(request, aircox) return
request.aircox = aircox
return self.get_response(request)

View File

@ -1,4 +1,4 @@
from .page import Page from .page import Page, NavItem
from .program import Program, Stream, Schedule from .program import Program, Stream, Schedule
from .episode import Episode, Diffusion from .episode import Episode, Diffusion
from .log import Log from .log import Log

View File

@ -25,6 +25,7 @@ class Episode(Page):
) )
objects = InProgramQuerySet.as_manager() objects = InProgramQuerySet.as_manager()
detail_url_name = 'episode-detail'
class Meta: class Meta:
verbose_name = _('Episode') verbose_name = _('Episode')
@ -36,12 +37,18 @@ class Episode(Page):
super().save(*args, **kwargs) super().save(*args, **kwargs)
@classmethod @classmethod
def from_date(cls, program, date): def get_default_title(cls, program, date):
title = settings.AIRCOX_EPISODE_TITLE.format( """ Get default Episode's title """
return settings.AIRCOX_EPISODE_TITLE.format(
program=program, program=program,
date=date.strftime(settings.AIRCOX_EPISODE_TITLE_DATE_FORMAT), date=date.strftime(settings.AIRCOX_EPISODE_TITLE_DATE_FORMAT),
) )
return cls(program=program, title=title)
@classmethod
def from_date(cls, program, date):
title = cls.get_default_title(program, date)
return cls(program=program, title=title, cover=program.cover)
class DiffusionQuerySet(BaseRerunQuerySet): class DiffusionQuerySet(BaseRerunQuerySet):
def episode(self, episode=None, id=None): def episode(self, episode=None, id=None):

View File

@ -3,14 +3,20 @@ from enum import IntEnum
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.text import slugify from django.utils.text import slugify
from django.utils.html import format_html
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from ckeditor.fields import RichTextField from ckeditor.fields import RichTextField
from filer.fields.image import FilerImageField from filer.fields.image import FilerImageField
from model_utils.managers import InheritanceQuerySet from model_utils.managers import InheritanceQuerySet
from .station import Station
__all__ = ['Page', 'PageQuerySet']
__all__ = ['PageQuerySet', 'Page', 'NavItem']
class PageQuerySet(InheritanceQuerySet): class PageQuerySet(InheritanceQuerySet):
@ -48,8 +54,10 @@ class Page(models.Model):
objects = PageQuerySet.as_manager() objects = PageQuerySet.as_manager()
detail_url_name = None
class Meta: class Meta:
abstract=True abstract = True
def __str__(self): def __str__(self):
return '{}: {}'.format(self._meta.verbose_name, return '{}: {}'.format(self._meta.verbose_name,
@ -64,10 +72,53 @@ class Page(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return reverse(self.detail_url_name, kwargs={'slug': self.slug}) \ return reverse(self.detail_url_name, kwargs={'slug': self.slug}) \
if self.is_published else '' if self.is_published else '#'
@property
def is_draft(self):
return self.status == self.STATUS.draft
@property @property
def is_published(self): def is_published(self):
return self.status == self.STATUS.published return self.status == self.STATUS.published
@property
def is_trash(self):
return self.status == self.STATUS.trash
class NavItem(models.Model):
""" Navigation menu items """
station = models.ForeignKey(
Station, models.CASCADE, verbose_name=_('station'))
menu = models.SlugField(_('menu'), max_length=24)
order = models.PositiveSmallIntegerField(_('order'))
text = models.CharField(_('title'), max_length=64)
url = models.CharField(_('url'), max_length=256, blank=True, null=True)
#target_type = models.ForeignKey(
# ContentType, models.CASCADE, blank=True, null=True)
#target_id = models.PositiveSmallIntegerField(blank=True, null=True)
#target = GenericForeignKey('target_type', 'target_id')
class Meta:
verbose_name = _('Menu item')
ordering = ('order', 'pk')
is_active = False
def get_is_active(self, url):
""" Return True if navigation item is active for this url. """
return self.url and url.startswith(self.url)
def render(self, request, css_class='', active_class=''):
if active_class and request.path.startswith(self.url):
css_class += ' ' + active_class
if not self.url:
return self.text
elif not css_class:
return format_html('<a href="{}">{}</a>', self.url, self.text)
else:
return format_html('<a href="{}" class="{}">{}</a>', self.url,
css_class, self.text)

View File

@ -62,6 +62,7 @@ class Program(Page):
) )
objects = ProgramQuerySet.as_manager() objects = ProgramQuerySet.as_manager()
detail_url_name = 'program-detail'
@property @property
def path(self): def path(self):

View File

@ -5,7 +5,9 @@ from django.db import models
from django.db.models import Q from django.db.models import Q
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import aircox.settings as settings from filer.fields.image import FilerImageField
from .. import settings
__all__ = ['Station', 'StationQuerySet', 'Port'] __all__ = ['Station', 'StationQuerySet', 'Port']
@ -45,6 +47,14 @@ class Station(models.Model):
default=True, default=True,
help_text=_('if checked, this station is used as the main one') help_text=_('if checked, this station is used as the main one')
) )
logo = FilerImageField(
on_delete=models.SET_NULL, null=True, blank=True,
verbose_name=_('Logo'),
)
hosts = models.TextField(
_("website's urls"), max_length=512, null=True, blank=True,
help_text=_('specify one url per line')
)
objects = StationQuerySet.as_manager() objects = StationQuerySet.as_manager()
@ -58,15 +68,14 @@ class Station(models.Model):
if self.default: if self.default:
qs = Station.objects.filter(default=True) qs = Station.objects.filter(default=True)
if self.pk is not None:
if self.pk:
qs = qs.exclude(pk=self.pk) qs = qs.exclude(pk=self.pk)
qs.update(default=False) qs.update(default=False)
super().save(*args, **kwargs) super().save(*args, **kwargs)
class Port (models.Model): class Port(models.Model):
""" """
Represent an audio input/output for the audio stream Represent an audio input/output for the audio stream
generation. generation.
@ -147,4 +156,3 @@ class Port (models.Model):
) )

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,347 @@
/******/ (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

@ -0,0 +1,84 @@
{% load static i18n thumbnail aircox %}
{% 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/main.css" %}"/>
<script src="{% static "aircox/main.js" %}"></script>
<script src="{% static "aircox/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="{{ station.logo.url }}" class="logo"/>
</a>
</div>
<div class="navbar-menu">
<div class="navbar-start">
{% block top-nav %}
{% nav_items "top" css_class="navbar-item" active_class="is-active" as items %}
{% for item, render in items %}
{{ render }}
{% endfor %}
{% endblock %}
</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.get_absolute_url }}">
&#10092; {{ parent.title }}</a></li>
</h4>
{% endif %}
{% endblock %}
</header>
{% block main %}{% endblock main %}
</main>
{% if show_side_nav %}
<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

@ -0,0 +1,59 @@
{% load i18n easy_thumbnails_tags aircox %}
{% comment %}
Context variables:
- object: the actual diffusion
- page: current parent page in which item is rendered
- hide_schedule: if True, do not display start time
- hide_headline: if True, do not display headline
{% endcomment %}
{% with object.episode as episode %}
{% with episode.program as program %}
<article class="media">
<div class="media-left">
<img src="{% thumbnail episode.cover 128x128 crop=scale %}"
class="small-cover">
</div>
<div class="media-content">
<h5 class="subtitle is-size-5">
{% if episode.is_published %}
<a href="{{ episode.get_absolute_url }}">{{ episode.title }}</a>
{% endif %}
</h5>
<div class="">
{% if not page or program != page %}
{% if program.is_published %}
<a href="{{ program.get_absolute_url }}" class="has-text-grey-dark">
{{ program.title }}</a>
{% else %}{{ program.title }}
{% endif %}
{% if not hide_schedule %} &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">
{{ episode.headline }}
</div>
{% endif %}
</div>
</article>
{% endwith %}
{% endwith %}

View File

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

View File

@ -0,0 +1,56 @@
{% extends "aircox/page.html" %}
{% load i18n aircox %}
{% 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/diffusion_item.html" %}
</div>
</div>
{% endfor %}
</div>
{% endfor %}
</template>
{% endwith %}
</a-tabs>
</section>
{% endblock %}

View File

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

View File

@ -0,0 +1,19 @@
{% 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

@ -0,0 +1,57 @@
{% extends "aircox/page.html" %}
{% load i18n aircox %}
{% block title %}
{% trans "Logs" %}
{% endblock %}
{% 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/diffusion_item.html" %}</td>
{% else %}
<td>
<time datetime="{{ object.date }}" title="{{ object.date }}">
{{ object.date|date:"H:i" }}
</time>
</td>
<td>{% include "aircox/log_item.html" %}</td>
{% endif %}
</tr>
{% endfor %}
</table>
{% endwith %}
</section>
{% endblock %}

View File

@ -0,0 +1,30 @@
{% extends "aircox/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 %}
{{ page.content|default_if_none:''|safe }}
{% endblock %}
{% endblock %}

View File

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

View File

@ -0,0 +1,29 @@
{% extends "aircox/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/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

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

View File

@ -0,0 +1,25 @@
{% 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

@ -0,0 +1,28 @@
import random
from django import template
from aircox.models import Page, Diffusion
random.seed()
register = template.Library()
@register.simple_tag(name='unique_id')
def do_unique_id(prefix=''):
value = str(random.random()).replace('.', '')
return prefix + '_' + value if prefix else value
@register.filter(name='is_diffusion')
def do_is_diffusion(obj):
return isinstance(obj, Diffusion)
@register.simple_tag(name='nav_items', takes_context=True)
def do_nav_items(context, menu, **kwargs):
station, request = context['station'], context['request']
return [(item, item.render(request, **kwargs))
for item in station.navitem_set.filter(menu=menu)]

View File

@ -1,10 +1,40 @@
from django.urls import path from django.urls import path, register_converter
from django.utils.translation import ugettext_lazy as _
from . import views
from .converters import PagePathConverter, DateConverter, WeekConverter
register_converter(PagePathConverter, 'page_path')
register_converter(DateConverter, 'date')
register_converter(WeekConverter, 'week')
#urls = [
# path('on_air', views.on_air, name='aircox.on_air'),
# path('monitor', views.Monitor.as_view(), name='aircox.monitor'),
# path('stats', views.StatisticsView.as_view(), name='aircox.stats'),
#]
import aircox.views as views
urls = [ urls = [
path('on_air', views.on_air, name='aircox.on_air'), path(_('programs/<slug:slug>/'),
path('monitor', views.Monitor.as_view(), name='aircox.monitor'), views.ProgramDetailView.as_view(), name='program-detail'),
path('stats', views.StatisticsView.as_view(), name='aircox.stats'), path(_('programs/<slug:program_slug>/episodes/'),
views.DiffusionListView.as_view(), name='diffusion-list'),
path(_('episodes/'),
views.DiffusionListView.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(_('logs/'), views.LogListView.as_view(), name='logs'),
path(_('logs/<date:date>/'), views.LogListView.as_view(), name='logs'),
# path('<page_path:path>', views.route_page, name='page'),
] ]

View File

@ -2,6 +2,23 @@ import datetime
import django.utils.timezone as tz import django.utils.timezone as tz
__all__ = ['Redirect', 'redirect', 'date_range', 'cast_date',
'date_or_default', 'to_timedelta', 'seconds_to_time']
class Redirect(Exception):
""" Redirect exception -- see `redirect()`. """
def __init__(self, url):
self.url = url
def redirect(url):
"""
Raise a Redirect exception in order to response a redirection to client.
AircoxMiddleware must be enabled. """
raise Redirect(url)
def date_range(date, delta=None, **delta_kwargs): def date_range(date, delta=None, **delta_kwargs):
""" """
Return a range of provided date such as `[date-delta, date+delta]`. Return a range of provided date such as `[date-delta, date+delta]`.
@ -53,6 +70,11 @@ def to_timedelta(time):
) )
def to_seconds(time):
""" Return total seconds for provided time """
return 3600 * time.hour + 60 * time.minute + time.second
def seconds_to_time(seconds): def seconds_to_time(seconds):
""" """
Seconds to datetime.time Seconds to datetime.time

View File

@ -2,7 +2,7 @@ import random
from django import template from django import template
from aircox import models as aircox from aircox.models import Page,
from aircox_web.models import Page from aircox_web.models import Page
random.seed() random.seed()
@ -12,11 +12,11 @@ register = template.Library()
@register.simple_tag(name='diffusion_page') @register.simple_tag(name='diffusion_page')
def do_diffusion_page(diffusion): def do_diffusion_page(diffusion):
""" Return page for diffusion. """ """ Return page for diffusion. """
diff = diffusion.initial if diffusion.initial is not None else diffusion episode = diffusion.episode
for obj in (diff, diffusion.program): if episode.is_publihed:
page = getattr(obj, 'page', None) return diff.episode
if page is not None and page.status == Page.STATUS.published: program = episode.program
return page return program if program.is_published else None
@register.simple_tag(name='unique_id') @register.simple_tag(name='unique_id')

5
assets/index.js Normal file
View File

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

14
assets/js/index.js Normal file
View File

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

6
assets/noscript.scss Normal file
View File

@ -0,0 +1,6 @@
/**[noscript="hidden"] {
display: none;
}*/

75
assets/styles.scss Normal file
View File

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

28
assets/vue/deck.vue Normal file
View File

@ -0,0 +1,28 @@
<template>
<div>
<a-tabs class="tabs" @select="value = $event.value;">
<template v-slot:default><slot name="tabs" :value="value"/></template>
</a-tabs>
<slot :value="value"></slot>
</div>
</template>
<script>
import Tabs from './tabs.vue';
export default {
props: ['default'],
data() {
return {
value: this.default,
}
},
components: {
'a-tabs': Tabs,
}
}
</script>

11
assets/vue/index.js Normal file
View File

@ -0,0 +1,11 @@
import Vue from 'vue';
import Tab from './tab.vue';
import Tabs from './tabs.vue';
Vue.component('a-tab', Tab);
Vue.component('a-tabs', Tabs);
export {Tab, Tabs};

31
assets/vue/tab.vue Normal file
View File

@ -0,0 +1,31 @@
<template>
<li @click.prevent="onclick"
:class="{'is-active': $parent.value == value}">
<slot></slot>
</li>
</template>
<script>
export default {
props: {
value: { default: undefined },
},
methods: {
select() {
this.$parent.selectTab(this);
},
onclick(event) {
this.select();
/*if(event.target.href != document.location)
window.history.pushState(
{ url: event.target.href },
event.target.innerText + ' - ' + document.title,
event.target.href
) */
}
}
}
</script>

45
assets/vue/tabs.vue Normal file
View File

@ -0,0 +1,45 @@
<template>
<div>
<div class="tabs is-centered">
<ul><slot name="tabs" :value="value" /></ul>
</div>
<slot :value="value"/>
</div>
</template>
<script>
export default {
props: {
default: { default: null },
},
data() {
return {
value: this.default,
}
},
computed: {
tab() {
const vnode = this.$slots.default && this.$slots.default.find(
elm => elm.child && elm.child.value == this.value
);
return vnode && vnode.child;
}
},
methods: {
selectTab(tab) {
const value = tab.value;
if(this.value === value)
return;
this.value = value;
this.$emit('select', {target: this, value: value, tab: tab});
},
},
}
</script>

View File

@ -13,12 +13,13 @@ Including another URLconf
1. Add an import: from blog import urls as blog_urls 1. Add an import: from blog import urls as blog_urls
2. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls)) 2. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls))
""" """
# from django.conf.urls.i18n import i18n_patterns
from django.conf import settings from django.conf import settings
from django.urls import include, path, re_path from django.urls import include, path, re_path
from django.contrib import admin from django.contrib import admin
import aircox.urls import aircox.urls
# import aircox_web.urls
try: try:
urlpatterns = [ urlpatterns = [
@ -35,7 +36,6 @@ try:
) )
urlpatterns.append(path('filer/', include('filer.urls'))) urlpatterns.append(path('filer/', include('filer.urls')))
# urlpatterns += aircox_web.urls.urlpatterns
except Exception as e: except Exception as e:
import traceback import traceback

29
package.json Normal file
View File

@ -0,0 +1,29 @@
{
"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"
}
}

93
webpack.config.js Normal file
View File

@ -0,0 +1,93 @@
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('aircox/static/aircox'),
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']
},
})