work on website; fix stuffs on aircox too

This commit is contained in:
bkfox
2019-07-22 01:37:25 +02:00
parent 08d1c7bfac
commit 3e432c42b0
35 changed files with 1089 additions and 416 deletions

View File

@ -15,6 +15,8 @@ from aircox.admin.mixins import UnrelatedInlineMixin
@admin.register(models.Site)
class SiteAdmin(ContentEditor):
list_display = ['title', 'station']
inlines = [
ContentEditorInline.create(models.SiteRichText),
ContentEditorInline.create(models.SiteImage),
@ -37,14 +39,26 @@ class PageDiffusionPlaylist(UnrelatedInlineMixin, TracksInline):
@admin.register(models.Page)
class PageAdmin(ContentEditor):
class PageAdmin(admin.ModelAdmin):
list_display = ["title", "parent", "status"]
list_editable = ['status']
prepopulated_fields = {"slug": ("title",)}
# readonly_fields = ('diffusion',)
fieldsets = (
(_('Main'), {
'fields': ['title', 'slug', 'as_program', 'headline'],
'fields': ['title', 'slug']
}),
(_('Settings'), {
'fields': ['status', 'static_path', 'path'],
}),
)
@admin.register(models.Article)
class ArticleAdmin(ContentEditor, PageAdmin):
fieldsets = (
(_('Main'), {
'fields': ['title', 'slug', 'as_program', 'cover', 'headline'],
'classes': ('tabbed', 'uncollapse')
}),
(_('Settings'), {
@ -59,36 +73,27 @@ class PageAdmin(ContentEditor):
)
inlines = [
ContentEditorInline.create(models.PageRichText),
ContentEditorInline.create(models.PageImage),
ContentEditorInline.create(models.ArticleRichText),
ContentEditorInline.create(models.ArticleImage),
]
@admin.register(models.DiffusionPage)
class DiffusionPageAdmin(PageAdmin):
fieldsets = copy.deepcopy(PageAdmin.fieldsets)
class DiffusionPageAdmin(ArticleAdmin):
fieldsets = copy.deepcopy(ArticleAdmin.fieldsets)
fieldsets[1][1]['fields'].insert(0, 'diffusion')
inlines = PageAdmin.inlines + [
PageDiffusionPlaylist
]
# TODO: permissions
#def get_inline_instances(self, request, obj=None):
# inlines = super().get_inline_instances(request, obj)
# if obj and obj.diffusion:
# inlines.insert(0, PageDiffusionPlaylist(self.model, self.admin_site))
# return inlines
def get_inline_instances(self, request, obj=None):
inlines = super().get_inline_instances(request, obj)
if obj and obj.diffusion:
inlines.insert(0, PageDiffusionPlaylist(self.model, self.admin_site))
return inlines
@admin.register(models.ProgramPage)
class DiffusionPageAdmin(PageAdmin):
fieldsets = copy.deepcopy(PageAdmin.fieldsets)
class ProgramPageAdmin(ArticleAdmin):
fieldsets = copy.deepcopy(ArticleAdmin.fieldsets)
fieldsets[1][1]['fields'].insert(0, 'program')
inlines = PageAdmin.inlines + [
PageDiffusionPlaylist
]

View File

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

View File

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

View File

@ -10,9 +10,12 @@ $body-background-color: $light;
}
.navbar.has-shadow {
box-shadow: 0em 0.1em 0.5em rgba(0,0,0,0.1);
box-shadow: 0em 0.05em 0.5em rgba(0,0,0,0.1);
}
/*
.navbar-brand img {
min-height: 6em;
}
@ -20,5 +23,22 @@ $body-background-color: $light;
.navbar-menu .navbar-item:not(:last-child) {
border-right: 1px $grey solid;
}
*/
/** page **/
img.cover {
border: 0.2em black solid;
}
.headline {
font-size: 1.2em;
padding: 0.2em 0em;
}
img.cover {
float: right;
max-width: 40%;
}

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};

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>

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>

49
aircox_web/converters.py Normal file
View File

@ -0,0 +1,49 @@
import datetime
from django.utils.safestring import mark_safe
from django.urls.converters import StringConverter
class PagePathConverter(StringConverter):
""" Match path for pages, including surrounding slashes. """
regex = r'/?|([-_a-zA-Z0-9]+/)*?'
def to_python(self, value):
if not value or value[0] != '/':
value = '/' + value
if len(value) > 1 and value[-1] != '/':
value = value + '/'
return value
def to_url(self, value):
if value[0] == '/':
value = value[1:]
if value[-1] != '/':
value = value + '/'
return mark_safe(value)
#class WeekConverter:
# """ Converter for date as YYYYY/WW """
# regex = r'[0-9]{4}/[0-9]{2}/?'
#
# def to_python(self, value):
# value = value.split('/')
# return datetime.date(int(value[0]), int(value[1]), int(value[2]))
#
# def to_url(self, value):
# return '{:04d}/{:02d}/'.format(*value.isocalendar())
class DateConverter:
""" Converter for date as YYYY/MM/DD """
regex = r'[0-9]{4}/[0-9]{2}/[0-9]{2}/?'
def to_python(self, value):
value = value.split('/')
return datetime.date(int(value[0]), int(value[1]), int(value[2]))
def to_url(self, value):
return '{:04d}/{:02d}/{:02d}/'.format(value.year, value.month,
value.day)

View File

@ -1,18 +1,19 @@
from django.core.validators import RegexValidator
from django.db import models
from django.db.models import F
from django.db.models import F, Q
from django.db.models.functions import Concat, Substr
from django.utils.translation import ugettext_lazy as _
from content_editor.models import Region, create_plugin_base
from model_utils.models import TimeStampedModel, StatusModel
from model_utils.managers import InheritanceManager
from model_utils.managers import InheritanceQuerySet
from model_utils import Choices
from filer.fields.image import FilerImageField
from aircox import models as aircox
from . import plugins
from .converters import PagePathConverter
class Site(models.Model):
@ -70,12 +71,31 @@ class SiteLink(plugins.Link, SitePlugin):
#-----------------------------------------------------------------------
class BasePage(StatusModel):
"""
Base abstract class for views whose url path is defined by users.
Page parenting is based on foreignkey to parent and page path.
class PageQueryset(InheritanceQuerySet):
def active(self):
return self.filter(Q(status=Page.STATUS.announced) |
Q(status=Page.STATUS.published))
Inspired by Feincms3.
def descendants(self, page, direct=True, inclusive=True):
qs = self.filter(parent=page) if direct else \
self.filter(path__startswith=page.path)
if not inclusive:
qs = qs.exclude(pk=page.pk)
return qs
def ancestors(self, page, inclusive=True):
path, paths = page.path, []
index = path.find('/')
while index != -1 and index+1 < len(path):
paths.append(path[0:index+1])
index = path.find('/', index+1)
return self.filter(path__in=paths)
class Page(StatusModel):
"""
Base class for views whose url path can be defined by users.
Page parenting is based on foreignkey to parent and page path.
"""
STATUS = Choices('draft', 'announced', 'published')
@ -89,22 +109,22 @@ class BasePage(StatusModel):
path = models.CharField(
_("path"), max_length=1000,
blank=True, db_index=True, unique=True,
validators=[
RegexValidator(
regex=r"^/(|.+/)$",
message=_("Path must start and end with a slash (/)."),
)
],
validators=[RegexValidator(
regex=PagePathConverter.regex,
message=_('Path accepts alphanumeric and "_-" characters '
'and must be surrounded by "/"')
)],
)
static_path = models.BooleanField(
_('static path'), default=False,
# FIXME: help
help_text=_('Update path using parent\'s page path and page title')
)
headline = models.TextField(
_('headline'), max_length=128, blank=True, null=True,
)
objects = InheritanceManager()
class Meta:
abstract = True
objects = PageQueryset.as_manager()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -112,10 +132,14 @@ class BasePage(StatusModel):
self._initial_parent = self.parent
self._initial_slug = self.slug
def view(self, request, *args, **kwargs):
def get_view_class(self):
""" Page view class"""
raise NotImplementedError('not implemented')
def view(self, request, *args, site=None, **kwargs):
""" Page view function """
from django.http import HttpResponse
return HttpResponse('Not implemented')
view = self.get_view_class().as_view(site=site, page=self)
return view(request, *args, **kwargs)
def update_descendants(self):
""" Update descendants pages' path if required. """
@ -123,8 +147,10 @@ class BasePage(StatusModel):
return
# FIXME: draft -> draft children?
expr = Concat(self.path, Substr(F('path'), len(self._initial_path)))
BasePage.objects.filter(path__startswith=self._initial_path) \
# FIXME: Page.objects (can't use Page since its an abstract model)
if len(self._initial_path):
expr = Concat('path', Substr(F('path'), len(self._initial_path)))
Page.objects.filter(path__startswith=self._initial_path) \
.update(path=expr)
def sync_generations(self, update_descendants=True):
@ -141,13 +167,13 @@ class BasePage(StatusModel):
if not self.title or not self.path or self.static_path and \
self.slug != self._initial_slug:
self.path = self.parent.path + '/' + self.slug \
self.path = self.parent.path + self.slug \
if self.parent is not None else '/' + self.slug
if self.path[-1] != '/':
self.path += '/'
if self.path[0] != '/':
self.path = '/' + self.path
if self.path[-1] != '/':
self.path += '/'
if update_descendants:
self.update_descendants()
@ -155,18 +181,23 @@ class BasePage(StatusModel):
self.sync_generations(update_descendants)
super().save(*args, **kwargs)
def __str__(self):
return '{}: {}'.format(self._meta.verbose_name,
self.title or self.pk)
class Page(BasePage, TimeStampedModel):
class Article(Page, TimeStampedModel):
""" User's pages """
regions = [
Region(key="main", title=_("Content")),
Region(key="content", title=_("Content")),
]
# metadata
as_program = models.ForeignKey(
aircox.Program, models.SET_NULL, blank=True, null=True,
related_name='published_pages',
limit_choices_to={'schedule__isnull': False},
# SO#51948640
# limit_choices_to={'schedule__isnull': False},
verbose_name=_('Show program as author'),
help_text=_("Show program as author"),
)
@ -180,45 +211,41 @@ class Page(BasePage, TimeStampedModel):
)
# content
headline = models.TextField(
_('headline'), max_length=128, blank=True, null=True,
)
cover = FilerImageField(
on_delete=models.SET_NULL, null=True, blank=True,
verbose_name=_('Cover'),
)
def get_view_class(self):
from .views import PageView
return PageView
def view(self, request, *args, **kwargs):
""" Page view function """
view = self.get_view_class().as_view()
return view(request, *args, **kwargs)
from .views import ArticleView
return ArticleView
class DiffusionPage(Page):
class DiffusionPage(Article):
diffusion = models.OneToOneField(
aircox.Diffusion, models.CASCADE,
blank=True, null=True,
related_name='page',
)
class ProgramPage(Page):
class ProgramPage(Article):
program = models.OneToOneField(
aircox.Program, models.CASCADE,
blank=True, null=True,
related_name='page',
)
def get_view_class(self):
from .views import ProgramView
return ProgramView
#-----------------------------------------------------------------------
PagePlugin = create_plugin_base(Page)
ArticlePlugin = create_plugin_base(Article)
class PageRichText(plugins.RichText, PagePlugin):
class ArticleRichText(plugins.RichText, ArticlePlugin):
pass
class PageImage(plugins.Image, PagePlugin):
class ArticleImage(plugins.Image, ArticlePlugin):
pass

View File

@ -18,6 +18,7 @@
"ttf-loader": "^1.0.2",
"vue-loader": "^15.7.0",
"vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.6.10",
"webpack": "^4.32.2",
"webpack-cli": "^3.3.2"
},

View File

@ -13,6 +13,6 @@ site_renderer.register(SiteLink, lambda plugin: plugin.render())
page_renderer = PluginRenderer()
page_renderer._renderers.clear()
page_renderer.register(PageRichText, lambda plugin: mark_safe(plugin.text))
page_renderer.register(PageImage, lambda plugin: plugin.render())
page_renderer.register(ArticleRichText, lambda plugin: mark_safe(plugin.text))
page_renderer.register(ArticleImage, lambda plugin: plugin.render())

View File

@ -0,0 +1,17 @@
{% extends "aircox_web/page.html" %}
{% block main %}
<section class="is-inline-block">
<img class="cover" src="{{ page.cover.url }}"/>
{% block headline %}
{{ page.headline }}
{% endblock %}
{% block content %}
{{ regions.main }}
{% endblock %}
</section>
{% endblock %}

View File

@ -1,50 +0,0 @@
{% load static i18n thumbnail %}
<html>
<head>
<meta charset="utf-8">
<meta name="application-name" content="aircox">
<meta name="description" content="{{ site.description }}">
<meta name="keywords" content="{{ site.tags }}">
<link rel="icon" href="{% thumbnail site.favicon 32x32 crop %}" />
{% block assets %}
<link rel="stylesheet" type="text/css" href="{% static "aircox_web/assets/main.css" %}"/>
<!-- <link rel="stylesheet" type="text/css" href="{% static "aircox_web/assets/vendor.css" %}"/> -->
<script src="{% static "aircox_web/assets/main.js" %}"></script>
<script src="{% static "aircox_web/assets/vendor.js" %}"></script>
{% endblock %}
<title>{% block title %}{{ site.title }}{% endblock %}</title>
{% block extra_head %}{% endblock %}
</head>
<body id="app">
<nav class="navbar has-shadow" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a href="/" title="{% trans "Home" %}" class="navbar-item">
<img src="{{ site.logo.url }}" class="logo"/>
</a>
</div>
<div class="navbar-menu">
<div class="navbar-start">
{{ site_regions.topnav }}
</div>
</div>
</nav>
<div class="columns">
<aside class="column">
{{ site_regions.sidenav }}
</aside>
<main class="column is-three-quarters">
{% block main %}
<h1>{{ page.title }}</h1>
{{ regions.main }}
{% endblock main %}
</main>
</div>
</body>
</html>

View File

@ -0,0 +1,43 @@
{% load i18n easy_thumbnails_tags aircox_web %}
{% comment %}
Context variables:
- object: the actual diffusion
- page: current parent page in which item is rendered
{% endcomment %}
{% with page as context_page %}
{% with object.program as program %}
{% diffusion_page object as page %}
<article class="media">
<div class="media-left">
<figure class="image is-64x64">
<img src="{% thumbnail page.cover|default:site.logo 128x128 crop=scale %}">
</figure>
</div>
<div class="media-content">
<div class="content">
<p>
{% if page and context_page != page %}
<strong><a href="{{ page.path }}">{{ page.title }}</a></strong>
{% else %}
<strong>{{ page.title|default:program.name }}</strong>
{% endif %}
{% if object.page is page %}
&mdash; <a href="{{ program.page.path }}">{{ program.name }}</a></small>
{% endif %}
{% if object.initial %}
{% with object.initial.date as date %}
<span class="tag is-info" title="{% blocktrans %}Rerun of {{ date }}{% endblocktrans %}">
{% trans "rerun" %}
</span>
{% endwith %}
{% endif %}
<br>
{{ page.headline|default:program.page.headline }}
</p>
</div>
</div>
</article>
{% endwith %}
{% endwith %}

View File

@ -0,0 +1,46 @@
{% extends "aircox_web/page.html" %}
{% load i18n aircox_web %}
{% block main %}
{{ block.super }}
<section class="section">
{% for object in object_list %}
<div class="columns">
<div class="column is-one-fifth has-text-right">
<time datetime="{{ object.start|date:"c" }}" title="{{ object.start }}">
{{ object.start|date:"d M, H:i" }}
</time>
</div>
<div class="column">
{% include "aircox_web/diffusion_item.html" %}
</div>
</div>
{% endfor %}
{% if is_paginated %}
<nav class="pagination is-centered" role="pagination" aria-label="{% trans "pagination" %}">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}" class="pagination-previous">
{% trans "Previous" %}</a>
{% endif %}
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}" class="pagination-next">
{% trans "Next" %}</a>
{% endif %}
<ul class="pagination-list">
{% for i in paginator.page_range %}
<li>
<a class="pagination-link {% if page_obj.number == i %}is-current{% endif %}"
href="?page={{ i }}">{{ i }}</a>
</li>
{% endfor %}
</ul>
</nav>
{% endif %}
</section>
{% endblock %}

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,51 @@
{% extends "aircox_web/page.html" %}
{% load i18n aircox_web %}
{% block main %}
{{ block.super }}
<section class="section">
{% if dates %}
<nav class="tabs is-centered" aria-label="{% trans "Other days' logs" %}">
<ul>
{% for day in dates %}
<li {% if day == date %}class="is-active"{% endif %}>
<a href="{% url "logs" date=day %}">
{{ day|date:"d b" }}
</a>
</li>
{% if forloop.last and day > min_date %}
<li>...</li>
{% endif %}
{% endfor %}
</ul>
</nav>
{% endif %}
{# <h4 class="subtitle size-4">{{ date }}</h4> #}
<table class="table is-striped is-hoverable is-fullwidth">
{% for object in object_list reversed %}
<tr>
{% if object|is_diffusion %}
<td>
<time datetime="{{ object.start }}" title="{{ object.start }}">
{{ object.start|date:"H:i" }} - {{ object.end|date:"H:i" }}
</time>
</td>
<td>{% include "aircox_web/diffusion_item.html" %}</td>
{% else %}
<td>
<time datetime="{{ object.date }}" title="{{ object.date }}">
{{ object.date|date:"H:i" }}
</time>
</td>
<td>{% include "aircox_web/log_item.html" %}</td>
{% endif %}
</tr>
{% endfor %}
</table>
</section>
{% endblock %}

View File

@ -1,8 +1,59 @@
{% extends "aircox_web/base.html" %}
{% load static i18n thumbnail %}
<html>
<head>
<meta charset="utf-8">
<meta name="application-name" content="aircox">
<meta name="description" content="{{ site.description }}">
<meta name="keywords" content="{{ site.tags }}">
<link rel="icon" href="{% thumbnail site.favicon 32x32 crop %}" />
{% block title %}{{ page.title }} -- {{ block.super }}{% endblock %}
{% block assets %}
<link rel="stylesheet" type="text/css" href="{% static "aircox_web/assets/main.css" %}"/>
<script src="{% static "aircox_web/assets/main.js" %}"></script>
<script src="{% static "aircox_web/assets/vendor.js" %}"></script>
{% endblock %}
<title>{% block title %}{% if title %}{{ title }} -- {% endif %}{{ site.title }}{% endblock %}</title>
{% block extra_head %}{% endblock %}
</head>
<body>
<div id="app">
<nav class="navbar has-shadow" role="navigation" aria-label="main navigation">
<div class="container">
<div class="navbar-brand">
<a href="/" title="{% trans "Home" %}" class="navbar-item">
<img src="{{ site.logo.url }}" class="logo"/>
</a>
</div>
<div class="navbar-menu">
<div class="navbar-start">
{{ site_regions.topnav }}
</div>
</div>
</div>
</nav>
<div class="container">
<div class="columns">
<aside class="column is-one-quarter">
{% block left-sidebar %}
{{ site_regions.sidenav }}
{% endblock %}
</aside>
<main class="column page">
<header class="header">
{% block header %}
<h1 class="title is-1">{{ title }}</h1>
{% endblock %}
</header>
{% block main %}{% endblock main %}
</main>
</div>
</div>
</div>
</body>
</html>
{% block main %}
<h1 class="title">{{ page.title }}</h1>
{% endblock %}

View File

@ -0,0 +1,27 @@
{% extends "aircox_web/article.html" %}
{% load i18n %}
{% block headline %}
<section class="is-size-5">
{% for schedule in program.schedule_set.all %}
<p>
<strong>{{ schedule.datetime|date:"l H:i" }}</strong>
<small>
{{ schedule.get_frequency_display }}
{% if schedule.initial %}
{% with schedule.initial.date as date %}
<span title="{% blocktrans %}Rerun of {{ date }}{% endblocktrans %}">
/ {% trans "rerun" %}
</span>
{% endwith %}
{% endif %}
</small>
</p>
{% endfor %}
</section>
{{ block.super }}
{% endblock %}

View File

@ -0,0 +1,52 @@
{% extends "aircox_web/page.html" %}
{% load i18n aircox_web %}
{% block main %}
{{ block.super }}
<section class="section">
<h3 class="subtitle size-3">
{% blocktrans %}From <b>{{ start }}</b> to <b>{{ end }}</b>{% endblocktrans %}
</h3>
{% unique_id "timetable" as timetable_id %}
<a-tabs default="{{ date }}">
<template v-slot:tabs="scope" noscript="hidden">
<li><a href="{% url "timetable" date=prev_date %}">&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>
<template v-slot:default="{value}">
{% for day, diffusions in by_date.items %}
<noscript><h4 class="subtitle is-4">{{ day|date:"l d F Y" }}</h4></noscript>
<div id="{{timetable_id}}-{{ day|date:"Y-m-d" }}" v-if="value == '{{ day }}'">
{% for object in diffusions %}
<div class="columns">
<div class="column is-one-fifth has-text-right">
<time datetime="{{ object.start|date:"c" }}">
{{ object.start|date:"H:i" }} - {{ object.end|date:"H:i" }}
</time>
</div>
<div class="column">
{% include "aircox_web/diffusion_item.html" %}
</div>
</div>
{% endfor %}
</div>
{% endfor %}
</template>
</a-tabs>
</section>
{% endblock %}

View File

@ -0,0 +1,28 @@
import random
from django import template
from aircox import models as aircox
from aircox_web.models import Page
random.seed()
register = template.Library()
@register.simple_tag(name='diffusion_page')
def do_diffusion_page(diffusion):
""" Return page for diffusion. """
for obj in (diffusion, diffusion.program):
page = getattr(obj, 'page', None)
if page is not None and page.status is not Page.STATUS.draft:
return page
@register.simple_tag(name='unique_id')
def do_unique_id(prefix=''):
value = str(random.random()).replace('.', '')
return prefix + '_' + value if prefix else value
@register.filter(name='is_diffusion')
def do_is_diffusion(obj):
return isinstance(obj, aircox.Diffusion)

View File

@ -1,9 +1,23 @@
from django.conf.urls import url
from django.urls import path, register_converter
from . import views
from . import views, models
from .converters import PagePathConverter, DateConverter
register_converter(PagePathConverter, 'page_path')
register_converter(DateConverter, 'date')
urlpatterns = [
url(r"^(?P<path>[-\w/]+)/$", views.route_page, name="page"),
url(r"^$", views.route_page, name="root"),
path('diffusions/',
views.TimetableView.as_view(), name='timetable'),
path('diffusions/<date:date>',
views.TimetableView.as_view(), name='timetable'),
path('diffusions/all',
views.DiffusionsView.as_view(), name='diffusion-list'),
path('diffusions/<slug:program>',
views.DiffusionsView.as_view(), name='diffusion-list'),
path('logs/', views.LogsView.as_view(), name='logs'),
path('logs/<date:date>', views.LogsView.as_view(), name='logs'),
path('<page_path:path>', views.route_page, name='page'),
]

View File

@ -1,48 +1,241 @@
from django.db.models import Q
from django.shortcuts import get_object_or_404, render
from django.views.generic.base import TemplateView
from collections import OrderedDict, deque
import datetime
from django.core.paginator import Paginator
from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext_lazy as _
from django.views.generic import TemplateView, ListView
from django.views.generic.base import TemplateResponseMixin, ContextMixin
from content_editor.contents import contents_for_item
from aircox import models as aircox
from .models import Site, Page
from .renderer import site_renderer, page_renderer
def route_page(request, path=None, *args, site=None, **kwargs):
def route_page(request, path=None, *args, model=None, site=None, **kwargs):
"""
Route request to page of the provided path. If model is provided, uses
it.
"""
# TODO/FIXME: django site framework | site from request host
# TODO: extra page kwargs (as in pepr)
site = Site.objects.all().order_by('-default').first() \
if site is None else site
model = model if model is not None else Page
page = get_object_or_404(
# TODO: published
Page.objects.select_subclasses()
.filter(Q(status=Page.STATUS.published) |
Q(status=Page.STATUS.announced)),
path="/{}/".format(path) if path else "/",
model.objects.select_subclasses().active(),
path=path
)
kwargs['page'] = page
return page.view(request, *args, site=site, **kwargs)
class PageView(TemplateView):
""" Base view class for pages. """
template_name = 'aircox_web/page.html'
class BaseView(TemplateResponseMixin, ContextMixin):
title = None
site = None
def dispatch(self, request, *args, site=None, **kwargs):
self.site = site if site is not None else \
Site.objects.all().order_by('-default').first()
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
if kwargs.get('site_regions') is None:
contents = contents_for_item(self.site, site_renderer._renderers.keys())
kwargs['site_regions'] = contents.render_regions(site_renderer)
kwargs.setdefault('site', self.site)
if self.title is not None:
kwargs.setdefault('title', self.title)
return super().get_context_data(**kwargs)
class ArticleView(BaseView, TemplateView):
""" Base view class for pages. """
template_name = 'aircox_web/article.html'
page = None
def get_context_data(self, **kwargs):
page = kwargs.setdefault('page', self.page or self.kwargs.get('site'))
site = kwargs.setdefault('site', self.site or self.kwargs.get('site'))
# article content
page = kwargs.setdefault('page', self.page or self.kwargs.get('page'))
if kwargs.get('regions') is None:
contents = contents_for_item(page, page_renderer._renderers.keys())
kwargs['regions'] = contents.render_regions(page_renderer)
if kwargs.get('site_regions') is None:
contents = contents_for_item(site, site_renderer._renderers.keys())
kwargs['site_regions'] = contents.render_regions(site_renderer)
kwargs.setdefault('title', page.title)
return super().get_context_data(**kwargs)
class ProgramView(ArticleView):
""" Base view class for pages. """
template_name = 'aircox_web/program.html'
next_diffs_count = 5
def get_context_data(self, program=None, **kwargs):
# TODO: pagination
program = program or self.page.program
#next_diffs = program.diffusion_set.on_air().after().order_by('start')
return super().get_context_data(
program=program,
# next_diffs=next_diffs[:self.next_diffs_count],
**kwargs,
)
class DiffusionView(ArticleView):
template_name = 'aircox_web/diffusion.html'
class DiffusionsView(BaseView, ListView):
template_name = 'aircox_web/diffusions.html'
model = aircox.Diffusion
paginate_by = 10
title = _('Diffusions')
program = None
# TODO: get program object + display program title when filtered by program
# TODO: pagination: in template, only a limited number of pages displayed
def get_queryset(self):
qs = super().get_queryset().station(self.site.station).on_air() \
.filter(initial__isnull=True) #TODO, page__isnull=False)
program = self.kwargs.get('program')
if program:
qs = qs.filter(program__page__slug=program)
return qs.order_by('-start')
class TimetableView(BaseView, ListView):
""" View for timetables """
template_name = 'aircox_web/timetable.html'
model = aircox.Diffusion
title = _('Timetable')
date = None
start = None
end = None
def get_queryset(self):
self.date = self.kwargs.get('date', datetime.date.today())
self.start = self.date - datetime.timedelta(days=self.date.weekday())
self.end = self.date + datetime.timedelta(days=7-self.date.weekday())
return super().get_queryset().station(self.site.station) \
.range(self.start, self.end) \
.order_by('start')
def get_context_data(self, **kwargs):
# regoup by dates
by_date = OrderedDict()
date = self.start
while date < self.end:
by_date[date] = []
date += datetime.timedelta(days=1)
for diffusion in self.object_list:
if not diffusion.date in by_date:
continue
by_date[diffusion.date].append(diffusion)
return super().get_context_data(
by_date=by_date,
date=self.date,
start=self.start,
end=self.end - datetime.timedelta(days=1),
prev_date=self.start - datetime.timedelta(days=1),
next_date=self.end + datetime.timedelta(days=1),
**kwargs
)
class LogViewBase(ListView):
station = None
date = None
delta = None
def get_queryset(self):
# only get logs for tracks: log for diffusion will be retrieved
# by the diffusions' queryset.
return super().get_queryset().station(self.station).on_air() \
.at(self.date).filter(track__isnull=False)
def get_diffusions_queryset(self):
return aircox.Diffusion.objects.station(self.station).on_air() \
.at(self.date)
def get_object_list(self, queryset):
diffs = deque(self.get_diffusions_queryset().order_by('start'))
logs = list(queryset.order_by('date'))
if not len(diffs):
return logs
object_list = []
diff = diffs.popleft()
last_collision = None
# diff.start < log on first diff
# diff.end > log on last diff
for index, log in enumerate(logs):
# get next diff
if diff.end < log.date:
diff = diffs.popleft() if len(diffs) else None
# no more diff that can collide: return list
if diff is None:
return object_list + logs[index:]
# diff colliding with log
if diff.start <= log.date <= diff.end:
if object_list[-1] is not diff:
object_list.append(diff)
last_collision = log
else:
# add last colliding log: track
if last_collision is not None:
object_list.append(last_collision)
object_list.append(log)
last_collision = None
return object_list
class LogsView(BaseView, LogViewBase):
""" View for timetables """
template_name = 'aircox_web/logs.html'
model = aircox.Log
title = _('Logs')
date = None
max_age = 10
min_date = None
def get(self, request, *args, **kwargs):
self.station = self.site.station
today = datetime.date.today()
self.min_date = today - datetime.timedelta(days=self.max_age)
self.date = min(max(self.min_date, self.kwargs['date']), today) \
if 'date' in self.kwargs else today
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
today = datetime.date.today()
max_date = min(max(self.date + datetime.timedelta(days=3),
self.min_date + datetime.timedelta(days=6)), today)
return super().get_context_data(
date=self.date,
min_date=self.min_date,
dates=(date for date in (
max_date - datetime.timedelta(days=i)
for i in range(0, 7)) if date >= self.min_date
),
object_list=self.get_object_list(self.object_list),
**kwargs
)

View File

@ -3,7 +3,7 @@ const webpack = require('webpack');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
// const { createLodashAliases } = require('lodash-loader');
const { VueLoaderPlugin } = require('vue-loader');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
module.exports = (env, argv) => Object({
@ -29,6 +29,13 @@ module.exports = (env, argv) => Object({
test: /[\\/]node_modules[\\/]/,
},
/*noscript: {
name: 'noscript',
chunks: 'initial',
enforce: true,
test: /noscript/,
}*/
}
}
},
@ -43,6 +50,7 @@ module.exports = (env, argv) => Object({
module: {
rules: [
{ test: /\.vue$/, loader: 'vue-loader' },
{
test: /\/node_modules\//,
sideEffects: false
@ -64,7 +72,6 @@ module.exports = (env, argv) => Object({
}
}],
},
{ test: /\.vue$/, use: 'vue-loader' },
],
},