work on pages, filters, lists

This commit is contained in:
bkfox 2019-09-09 02:47:57 +02:00
parent c68e21ee57
commit 215a6ac331
45 changed files with 424 additions and 275 deletions

View File

@ -11,11 +11,8 @@ __all__ = ['ArticleAdmin']
@admin.register(Article)
class ArticleAdmin(PageAdmin):
list_display = PageAdmin.list_display + ('program',)
list_filter = PageAdmin.list_filter + ('program',)
search_fields = PageAdmin.search_fields + ['program__title']
list_filter = PageAdmin.list_filter
search_fields = PageAdmin.search_fields + ['parent__title']
# TODO: readonly field
fieldsets = copy.deepcopy(PageAdmin.fieldsets)
fieldsets[1][1]['fields'].insert(0, 'program')

View File

@ -48,13 +48,11 @@ class DiffusionInline(DiffusionBaseAdmin, admin.TabularInline):
@admin.register(Episode)
class EpisodeAdmin(PageAdmin):
list_display = PageAdmin.list_display + ('program',)
list_filter = PageAdmin.list_filter + ('program',)
search_fields = PageAdmin.search_fields + ['program__title']
readonly_fields = ('program',)
list_display = PageAdmin.list_display
list_filter = PageAdmin.list_filter
search_fields = PageAdmin.search_fields + ['parent__title']
# readonly_fields = ('parent',)
fieldsets = copy.deepcopy(PageAdmin.fieldsets)
fieldsets[1][1]['fields'].insert(0, 'program')
inlines = [TracksInline, SoundInline, DiffusionInline]

View File

@ -21,7 +21,7 @@ class CategoryAdmin(admin.ModelAdmin):
# limit category choice
class PageAdmin(admin.ModelAdmin):
list_display = ('cover_thumb', 'title', 'status', 'category')
list_display = ('cover_thumb', 'title', 'status', 'category', 'parent')
list_display_links = ('cover_thumb', 'title')
list_editable = ('status', 'category')
list_filter = ('status', 'category')
@ -33,7 +33,7 @@ class PageAdmin(admin.ModelAdmin):
'fields': ['title', 'slug', 'category', 'cover', 'content'],
}),
(_('Publication Settings'), {
'fields': ['featured', 'allow_comments', 'status'],
'fields': ['featured', 'allow_comments', 'status', 'parent'],
'classes': ('collapse',),
}),
]

View File

@ -33,7 +33,8 @@ class WeekConverter:
return datetime.datetime.strptime(value + '/1', '%G/%V/%u').date()
def to_url(self, value):
return '{:04d}/{:02d}'.format(*value.isocalendar())
return value if isinstance(value, str) else \
'{:04d}/{:02d}'.format(*value.isocalendar())
class DateConverter:
@ -41,8 +42,9 @@ class DateConverter:
regex = r'[0-9]{4}/[0-9]{2}/[0-9]{2}'
def to_python(self, value):
return str_to_date(value)
value = value.split('/')[:3]
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)
return value if isinstance(value, str) else \
'{:04d}/{:02d}/{:02d}'.format(value.year, value.month, value.day)

View File

@ -2,28 +2,20 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _
from .page import Page, PageQuerySet
from .program import Program, InProgramQuerySet
class ArticleQuerySet(InProgramQuerySet, PageQuerySet):
pass
from .program import Program, ProgramChildQuerySet
class Article(Page):
detail_url_name = 'article-detail'
program = models.ForeignKey(
Program, models.SET_NULL,
verbose_name=_('program'), blank=True, null=True,
help_text=_("publish as this program's article"),
)
is_static = models.BooleanField(
_('is static'), default=False,
help_text=_('Should this article be considered as a page '
'instead of a blog article'),
)
objects = ArticleQuerySet.as_manager()
objects = ProgramChildQuerySet.as_manager()
class Meta:
verbose_name = _('Article')

View File

@ -8,7 +8,7 @@ from django.utils.functional import cached_property
from aircox import settings, utils
from .program import Program, InProgramQuerySet, \
from .program import Program, ProgramChildQuerySet, \
BaseRerun, BaseRerunQuerySet
from .page import Page, PageQuerySet
@ -16,18 +16,18 @@ from .page import Page, PageQuerySet
__all__ = ['Episode', 'Diffusion', 'DiffusionQuerySet']
class EpisodeQuerySet(PageQuerySet, InProgramQuerySet):
pass
class Episode(Page):
program = models.ForeignKey(
Program, models.CASCADE,
verbose_name=_('program'),
)
objects = EpisodeQuerySet.as_manager()
objects = ProgramChildQuerySet.as_manager()
detail_url_name = 'episode-detail'
item_template_name = 'aircox/episode_item.html'
@property
def program(self):
return getattr(self.parent, 'program', None)
@program.setter
def program(self, value):
self.parent = value
class Meta:
verbose_name = _('Episode')
@ -41,6 +41,8 @@ class Episode(Page):
def save(self, *args, **kwargs):
if self.cover is None:
self.cover = self.program.cover
if self.parent is None:
raise ValueError('missing parent program')
super().save(*args, **kwargs)
@classmethod
@ -155,6 +157,8 @@ class Diffusion(BaseRerun):
# help_text = _('use this input port'),
# )
item_template_name = 'aircox/diffusion_item.html'
class Meta:
verbose_name = _('Diffusion')
verbose_name_plural = _('Diffusions')

View File

@ -46,6 +46,11 @@ class PageQuerySet(InheritanceQuerySet):
def trash(self):
return self.filter(status=Page.STATUS_TRASH)
def parent(self, parent=None, id=None):
""" Return pages having this parent. """
return self.filter(parent=parent) if id is None else \
self.filter(parent__id=id)
class Page(models.Model):
""" Base class for publishable content """
@ -58,6 +63,8 @@ class Page(models.Model):
(STATUS_TRASH, _('trash')),
)
parent = models.ForeignKey('self', models.CASCADE, blank=True, null=True,
related_name='child_set')
title = models.CharField(max_length=128)
slug = models.SlugField(_('slug'), blank=True, unique=True)
status = models.PositiveSmallIntegerField(
@ -74,7 +81,7 @@ class Page(models.Model):
content = RichTextField(
_('content'), blank=True, null=True,
)
date = models.DateTimeField(default=tz.now)
pub_date = models.DateTimeField(blank=True, null=True)
featured = models.BooleanField(
_('featured'), default=False,
)
@ -85,17 +92,19 @@ class Page(models.Model):
objects = PageQuerySet.as_manager()
detail_url_name = None
item_template_name = 'aircox/page_item.html'
def __str__(self):
return '{}: {}'.format(self._meta.verbose_name,
self.title or self.pk)
return '{}'.format(self.title or self.pk)
def save(self, *args, **kwargs):
# TODO: ensure unique slug
if not self.slug:
self.slug = slugify(self.title)
print(self.title, '--', self.slug)
if self.is_published and self.pub_date is None:
self.pub_date = tz.datetime.now()
elif not self.is_published:
self.pub_date = None
super().save(*args, **kwargs)
def get_absolute_url(self):

View File

@ -23,7 +23,8 @@ from .station import Station
logger = logging.getLogger('aircox')
__all__ = ['Program', 'ProgramQuerySet', 'Stream', 'Schedule']
__all__ = ['Program', 'ProgramQuerySet', 'Stream', 'Schedule',
'ProgramChildQuerySet', 'BaseRerun', 'BaseRerunQuerySet']
class ProgramQuerySet(PageQuerySet):
@ -49,15 +50,8 @@ class Program(Page):
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'),
on_delete=models.CASCADE,
)
station = models.ForeignKey(Station, models.CASCADE,
verbose_name=_('station'))
active = models.BooleanField(
_('active'),
default=True,
@ -146,10 +140,17 @@ class Program(Page):
.update(path=Concat('path', Substr(F('path'), len(path_))))
class InProgramQuerySet(models.QuerySet):
"""
Queryset for model having a ForeignKey field "program" to `Program`.
"""
class ProgramChildQuerySet(PageQuerySet):
def station(self, station=None, id=None):
return self.filter(parent__program__station=station) if id is None else \
self.filter(parent__program__station__id=id)
def program(self, program=None, id=None):
return self.parent(program, id)
class BaseRerunQuerySet(models.QuerySet):
""" Queryset for BaseRerun (sub)classes. """
def station(self, station=None, id=None):
return self.filter(program__station=station) if id is None else \
self.filter(program__station__id=id)
@ -158,9 +159,6 @@ class InProgramQuerySet(models.QuerySet):
return self.filter(program=program) if id is None else \
self.filter(program__id=id)
class BaseRerunQuerySet(InProgramQuerySet):
""" Queryset for BaseRerun (sub)classes. """
def rerun(self):
return self.filter(initial__isnull=False)

View File

@ -93,7 +93,7 @@ def schedule_pre_delete(sender, instance, *args, **kwargs):
@receiver(signals.post_delete, sender=Diffusion)
def diffusion_post_delete(sender, instance, *args, **kwargs):
Episode.objects.filter(diffusion__isnull=True, content_isnull=True,
Episode.objects.filter(diffusion__isnull=True, content__isnull=True,
sound__isnull=True) \
.delete()

View File

@ -16,6 +16,9 @@
width: 100%;
margin: 1em 0em; }
ul.menu-list li {
list-style-type: none; }
@keyframes spinAround {
from {
transform: rotate(0deg); }
@ -7180,6 +7183,11 @@ label.panel-block {
.has-background-transparent {
background-color: transparent; }
.is-opacity-light {
opacity: 0.7; }
.is-opacity-light:hover {
opacity: 1; }
.navbar + .container {
margin-top: 1em; }
@ -7192,6 +7200,29 @@ a.navbar-item.is-active {
.navbar .navbar-dropdown {
z-index: 2000; }
.navbar .navbar-split {
margin: 0.2em 0em;
margin-right: 1em;
padding-right: 1em;
border-right: 1px #b5b5b5 solid;
display: inline-block; }
.navbar form {
margin: 0em;
padding: 0em; }
.filters {
margin: 1em 0em;
background-color: transparent;
margin-bottom: 1em; }
.filters .title {
padding-right: 2em;
margin-right: 1em;
border-right: 1px #b5b5b5 solid;
font-size: 1.25rem;
color: #7a7a7a;
font-weight: 300; }
/*
.navbar-brand img {
min-height: 6em;

View File

@ -305,7 +305,7 @@ eval("// extracted by mini-css-extract-plugin\n\n//# sourceURL=webpack:///./asse
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n//\n//\n//\n//\n//\n//\n\n\nconst splitReg = new RegExp(`,\\s*`, 'g');\n\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n data() {\n return {\n counts: {},\n }\n },\n\n methods: {\n update() {\n const items = this.$el.querySelectorAll('input[name=\"data\"]:checked')\n const counts = {};\n\n console.log(items)\n for(var item of items)\n if(item.value)\n for(var tag of item.value.split(splitReg))\n counts[tag.trim()] = (counts[tag.trim()] || 0) + 1;\n this.counts = counts;\n console.log('counts', this.counts)\n }\n },\n\n mounted() {\n this.$refs.form.addEventListener('change', () => this.update())\n this.update()\n }\n});\n\n\n//# sourceURL=webpack:///./assets/admin/statistics.vue?./node_modules/vue-loader/lib??vue-loader-options");
eval("__webpack_require__.r(__webpack_exports__);\n//\n//\n//\n//\n//\n//\n\n\nconst splitReg = new RegExp(`,\\s*`, 'g');\n\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n data() {\n return {\n counts: {},\n }\n },\n\n methods: {\n update() {\n const items = this.$el.querySelectorAll('input[name=\"data\"]:checked')\n const counts = {};\n\n console.log(items)\n for(var item of items)\n if(item.value)\n for(var tag of item.value.split(splitReg))\n counts[tag.trim()] = (counts[tag.trim()] || 0) + 1;\n this.counts = counts;\n }\n },\n\n mounted() {\n this.$refs.form.addEventListener('change', () => this.update())\n this.update()\n }\n});\n\n\n//# sourceURL=webpack:///./assets/admin/statistics.vue?./node_modules/vue-loader/lib??vue-loader-options");
/***/ }),

View File

@ -7162,6 +7162,11 @@ label.panel-block {
.has-background-transparent {
background-color: transparent; }
.is-opacity-light {
opacity: 0.7; }
.is-opacity-light:hover {
opacity: 1; }
.navbar + .container {
margin-top: 1em; }
@ -7174,6 +7179,29 @@ a.navbar-item.is-active {
.navbar .navbar-dropdown {
z-index: 2000; }
.navbar .navbar-split {
margin: 0.2em 0em;
margin-right: 1em;
padding-right: 1em;
border-right: 1px #b5b5b5 solid;
display: inline-block; }
.navbar form {
margin: 0em;
padding: 0em; }
.filters {
margin: 1em 0em;
background-color: transparent;
margin-bottom: 1em; }
.filters .title {
padding-right: 2em;
margin-right: 1em;
border-right: 1px #b5b5b5 solid;
font-size: 1.25rem;
color: #7a7a7a;
font-weight: 300; }
/*
.navbar-brand img {
min-height: 6em;

View File

@ -4,7 +4,9 @@
{% block content %}{{ block.super }}
{# TODO: date subtitle #}
<a-statistics>
<div class="columns">
<a-statistics class="column">
<template v-slot:default="{counts}">
<table class="table is-hoverable is-fullwidth">
<thead>
@ -63,6 +65,15 @@
</template>
</a-statistics>
<nav class="column menu is-one-fifth-desktop" role="menu">
{% with "admin:tools-stats" as url_name %}
{% include "aircox/widgets/dates_menu.html" %}
{% endwith %}
</nav>
</div>
{% endblock %}

View File

@ -46,7 +46,7 @@
<a class="navbar-link" href="{% url "admin:aircox_article_changelist" %}">{% trans "Articles" %}</a>
<div class="navbar-dropdown is-boxed is-right">
{% for program in programs %}
<a class="navbar-item" href="{% url "admin:aircox_article_changelist" %}?program={{ program.pk }}">
<a class="navbar-item" href="{% url "admin:aircox_article_changelist" %}?parent={{ program.pk }}">
{{ program.title }}</a>
{% endfor %}
</div>
@ -56,7 +56,7 @@
<a class="navbar-link" href="{% url "admin:aircox_episode_changelist" %}">{% trans "Episodes" %}</a>
<div class="navbar-dropdown is-boxed is-right">
{% for program in programs %}
<a class="navbar-item" href="{% url "admin:aircox_episode_changelist" %}?program={{ program.pk }}">
<a class="navbar-item" href="{% url "admin:aircox_episode_changelist" %}?parent={{ program.pk }}">
{{ program.title }}</a>
{% endfor %}
</div>

View File

@ -1,14 +1,14 @@
{% extends "aircox/page_detail.html" %}
{% load i18n %}
{% block side_nav %}
{% block sidebar %}
{{ block.super }}
{% if side_items %}
{% if sidebar_items %}
<section>
<h4 class="title is-4">{% trans "Latest news" %}</h4>
{% for object in side_items %}
{% for object in sidebar_items %}
{% include "aircox/page_item.html" %}
{% endfor %}

View File

@ -3,6 +3,7 @@
Context:
- cover: image cover
- site: current website
- has_filters: display filter bar (using block "filters")
{% endcomment %}
<html>
<head>
@ -71,19 +72,50 @@ Context:
{% endblock %}
</header>
{% if has_filters %}
<nav class="navbar filters"
aria-label="{% trans "list filters" %}">
{% block filters %}{% endblock %}
</nav>
{% endif %}
{% block main %}{% endblock main %}
</main>
{% if show_side_nav %}
{% if has_sidebar %}
<aside class="column is-one-third-desktop">
{# FIXME: block cover into side_nav one #}
{# FIXME: block cover into sidebar one #}
{% block cover %}
{% if cover is not None %}
<img class="cover" src="{{ cover.url }}" class="cover"/>
{% endif %}
{% endblock %}
{% block side_nav %}
{% block sidebar %}
{% if sidebar_items %}
<section>
<h4 class="title is-4">
{% block sidebar_title %}{% trans "Recently" %}{% endblock %}
</h4>
{% for object in sidebar_items %}
{% include "aircox/episode_item.html" %}
{% endfor %}
<br>
<nav class="pagination is-centered">
<ul class="pagination-list">
<li>
<a {% if parent %}href="{% url "page-list" parent_slug=parent.slug %}"{% else %}href="{% url "page-list" %}"{% endif %}
class="pagination-link"
aria-label="{% trans "Show all program's diffusions" %}">
{% trans "Show more" %}
</a>
</li>
</ul>
</nav>
{% endif %}
</section>
{% endblock %}
</aside>
{% endif %}

View File

@ -0,0 +1,11 @@
{% comment %}
Context:
- object: diffusion
- "episode_item"'s context (except object and diffusion)
{% endcomment %}
{% with object as diffusion %}
{% with object.episode as object %}
{% include "aircox/episode_item.html" %}
{% endwith %}
{% endwith %}

View File

@ -1,5 +1,5 @@
{% extends "aircox/page.html" %}
{% load i18n aircox %}
{% load i18n aircox humanize %}
{% block title %}
{% with station.name as station %}
@ -9,17 +9,22 @@
{% block subtitle %}{{ date|date:"l d F Y" }}{% endblock %}
{% block main %}{{ block.super }}
<div class="columns">
{% block filters %}
{% with "diffusion-list" as url_name %}
{% include "aircox/widgets/dates_menu.html" %}
{% endwith %}
{% endblock %}
{% block main %}{{ block.super }}
{% with True as hide_schedule %}
<section class="column">
<section role="list">
<div id="timetable-{{ date|date:"Y-m-d" }}">
{% for diffusion in object_list %}
<div class="columns">
{# FIXME: opacity should work -- maybe hidden tz #}
<div class="columns {% if diffusion.start.date != date and diffusion.start.end <= date %}is-opacity-light{% endif %}">
<div class="column is-one-fifth has-text-right">
<time datetime="{{ diffusion.start|date:"c" }}">
{{ diffusion.start|date:"H:i" }} - {{ diffusion.end|date:"H:i" }}
{{ diffusion.start|date:"d H:i" }} - {{ diffusion.end|date:"d H:i" }}
</time>
</div>
<div class="column">
@ -33,12 +38,10 @@
</section>
{% endwith %}
{% comment %}
<nav class="column menu is-one-third-desktop" role="menu">
{% with "diffusion-list" as url_name %}
{% include "aircox/widgets/dates_menu.html" %}
{% endwith %}
</nav>
{% endcomment %}
</div>
{% endblock %}

View File

@ -11,9 +11,7 @@ for design review.
{% if object|is_diffusion %}
{% with object as diffusion %}
{% with diffusion.episode as object %}
{% include "aircox/episode_item.html" %}
{% endwith %}
{% include "aircox/diffusion_item.html" %}
{% endwith %}
{% else %}
{% with object.track as object %}

View File

@ -10,14 +10,17 @@
{% block subtitle %}{{ date|date:"l d F Y" }}{% endblock %}
{% block filters %}
{% with "log-list" as url_name %}
{% include "aircox/widgets/dates_menu.html" %}
{% endwith %}
{% endblock %}
{% block main %}
<div class="columns">
<section class="section column">
<section>
{# <h4 class="subtitle size-4">{{ date }}</h4> #}
{% with True as hide_schedule %}
<table class="table is-striped is-hoverable is-fullwidth has-background-transparent">
<table class="table is-striped is-hoverable is-fullwidth">
{% for object in object_list %}
<tr>
<td>
@ -37,13 +40,5 @@
</table>
{% endwith %}
</section>
<nav class="column menu is-one-third-desktop" role="menu">
{% with "logs" as url_name %}
{% include "aircox/widgets/dates_menu.html" %}
{% endwith %}
</nav>
</div>
{% endblock %}

View File

@ -2,63 +2,65 @@
{% load i18n aircox %}
{% block title %}
{{ view.model|verbose_name:True|title }}
{% if not parent %}{{ view.model|verbose_name:True|title }}
{% else %}
{% with parent.title as title %}
{% blocktrans %}Publications of {{ title }}{% endblocktrans %}
{% endwith %}
{% endif %}
{% 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="">
{% block list_filters %}
<div class="field is-horizontal">
<div class="field-label">
<label class="label">{% trans "Categories" %}</label>
</div>
<div class="field-body">
<div class="field is-grouped is-narrow">
<div class="control">
{% for category in filter_categories %}
<label class="checkbox">
<input type="checkbox" class="checkbox" name="categories"
value="{{ category.slug }}"
{% if category.slug in categories %}checked{% endif %} />
{{ category.title }}
</label>
{% endfor %}
{% block filters %}
<div class="navbar-branding">
<h4 class="navbar-item title">{% trans "Filters" %}</h4>
</div>
<form method="GET" action="" class="navbar-menu">
<div class="navbar-start">
<div class="navbar-item">
{% block list_filters %}
<div class="field is-horizontal">
<div class="field-label">
<label class="label">{% trans "Categories" %}</label>
</div>
<div class="field-body">
<div class="field is-grouped is-narrow">
<div class="control">
{% for category in filter_categories %}
<label class="checkbox">
<input type="checkbox" class="checkbox" name="categories"
value="{{ category.slug }}"
{% if category.slug in categories %}checked{% endif %} />
{{ category.title }}
</label>
{% endfor %}
</div>
</div>
</div>
</div>
{% endblock %}
</div>
{% endblock %}
<div class="field is-horizontal">
<div class="field-label">
<label class="label"></label>
</div>
<div class="field-body">
<div class="field is-grouped is-grouped-right">
<div class="control">
<button class="button is-primary"/>{% trans "Apply" %}</button>
</div>
<div class="control">
<a href="?" class="button is-secondary">{% trans "Reset" %}</a>
</div>
</div>
<div class="navbar-end">
<div class="navbar-item">
<div class="field is-grouped is-grouped-right">
<div class="control">
<button class="button is-primary"/>{% trans "Apply" %}</button>
</div>
<div class="control">
<a href="?" class="button is-secondary">{% trans "Reset" %}</a>
</div>
</div>
</div>
</form>
</section>
{% endif %}
</div>
</form>
{% endblock %}
{% block main %}
<section>
<section role="list">
{% for object in object_list %}
{% block list_object %}
{% include item_template_name %}
{% include object.item_template_name|default:item_template_name %}
{% endblock %}
{% endfor %}
</section>

View File

@ -1,30 +1,16 @@
{% extends "aircox/page_detail.html" %}
{% load i18n %}
{% block side_nav %}
{{ block.super }}
{% if side_items %}
<section>
<h4 class="title is-4">{% trans "Last shows" %}</h4>
{% for object in side_items %}
{% include "aircox/episode_item.html" %}
{% endfor %}
<br>
<nav class="pagination is-centered">
<ul class="pagination-list">
<li>
<a href="{% url "episode-list" parent_slug=program.slug %}"
class="pagination-link"
aria-label="{% trans "Show all program's diffusions" %}">
{% trans "More shows" %}
</a>
</li>
</ul>
</nav>
</section>
{% endif %}
{% block sidebar_title %}
{% with program.title as program %}
{% blocktrans %} Recently on {{ program }}{% endblocktrans %}
{% endwith %}
{% endblock %}
{% block sidebar %}
{% with program as parent %}
{{ block.super }}
{% endwith %}
{% endblock %}

View File

@ -4,9 +4,9 @@ Context:
{% endcomment %}
<span class="has-text-info is-size-5">&#9836;</span>
<span>{{ track.title }}</span>
<span>{{ object.title }}</span>
<span class="has-text-grey-dark has-text-weight-light">
&mdash; {{ track.artist }}
{% if track.info %}(<i>{{ track.info }}</i>){% endif %}
&mdash; {{ object.artist }}
{% if object.info %}(<i>{{ object.info }}</i>){% endif %}
</span>

View File

@ -9,40 +9,35 @@ Context:
An empty date results to a title or a separator
{% endcomment %}
{% load i18n humanize %}
{% load i18n %}
<nav class="menu is-one-third-desktop " role="menu"
<div class="navbar-menu" role="menu"
aria-label="{% trans "pick a date" %}">
<p class="menu-label">{% trans "Pick a date" %}</p>
<form action="{% url url_name %}" method="GET"
aria-label="{% trans "Jump to date" %}">
<div class="field has-addons">
<div class="control has-icons-left">
<span class="icon is-small is-left"><span class="far fa-calendar"></span></span>
<input type="{{ date_input|default:"date" }}" class="input date"
name="date" value="{{ date|date:"Y-m-d" }}">
</div>
<div class="control">
<button class="button is-primary">{% trans "Go" %}</button>
</div>
</div>
</form>
<ul class="menu-list">
{% for day, title in dates %}
{% if not day %}
{% if title %}
</ul>
<p class="menu-label">{{ title }}</p>
<ul class="menu-list">
{% else %}<hr class="dropdown-divider">{% endif %}
{% else %}
<li><a href="{% url url_name date=day %}" {% if day == date %}class="is-active"{% endif %}>
{% if title %}{{ title }}{% else %}{{ day|naturalday:"l d" }}{% endif %}
</a></li>
{% endif %}
<div class="navbar-start">
{% for day in dates %}
<a href="{% url url_name date=day %}" class="navbar-item {% if day == date %}is-active{% endif %}">
{{ day|date:"D. d" }}
</a>
{% endfor %}
</ul>
</nav>
</div>
<div class="navbar-end">
<div class="navbar-item">
<form action="{% url url_name %}" method="GET" class="navbar-body"
aria-label="{% trans "Jump to date" %}">
<div class="field has-addons">
<div class="control has-icons-left">
<span class="icon is-small is-left"><span class="far fa-calendar"></span></span>
<input type="{{ date_input|default:"date" }}" class="input date"
name="date" value="{{ date|date:"Y-m-d" }}">
</div>
<div class="control">
<button class="button is-primary">{% trans "Go" %}</button>
</div>
</div>
</form>
</div>
</div>
</div>

View File

@ -24,6 +24,8 @@ api = [
urls = [
path(_(''),
views.DiffusionListView.as_view(), name='home'),
path('api/', include(api)),
# path('', views.PageDetailView.as_view(model=models.Article),
@ -35,15 +37,6 @@ urls = [
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:parent_slug>/episodes/'),
views.EpisodeListView.as_view(), name='episode-list'),
path(_('programs/<slug:parent_slug>/articles/'),
views.ArticleListView.as_view(), name='article-list'),
path(_('episodes/'),
views.EpisodeListView.as_view(), name='episode-list'),
path(_('episodes/<slug:slug>/'),
@ -53,7 +46,23 @@ urls = [
path(_('week/<date:date>/'),
views.DiffusionListView.as_view(), name='diffusion-list'),
path(_('logs/'), views.LogListView.as_view(), name='logs'),
path(_('logs/<date:date>/'), views.LogListView.as_view(), name='logs'),
path(_('logs/'), views.LogListView.as_view(), name='log-list'),
path(_('logs/<date:date>/'), views.LogListView.as_view(), name='log-list'),
# path('<page_path:path>', views.route_page, name='page'),
path(_('publications/'),
views.ProgramPageListView.as_view(), name='page-list'),
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:parent_slug>/episodes/'),
views.EpisodeListView.as_view(), name='episode-list'),
path(_('programs/<slug:parent_slug>/articles/'),
views.ArticleListView.as_view(), name='article-list'),
path(_('programs/<slug:parent_slug>/publications/'),
views.ProgramPageListView.as_view(), name='page-list'),
]

View File

@ -5,5 +5,5 @@ from .base import BaseView
from .episode import EpisodeDetailView, EpisodeListView, DiffusionListView
from .log import LogListView
from .page import PageDetailView, PageListView
from .program import ProgramDetailView
from .program import ProgramDetailView, ProgramPageListView

View File

@ -49,6 +49,9 @@ class AdminSite(admin.AdminSite):
path('tools/statistics/',
self.admin_view(StatisticsView.as_view()),
name='tools-stats'),
path('tools/statistics/<date:date>/',
self.admin_view(StatisticsView.as_view()),
name='tools-stats'),
]
return urls

View File

@ -7,10 +7,10 @@ __all__ = ['ArticleDetailView', 'ArticleListView']
class ArticleDetailView(PageDetailView):
show_side_nav = True
has_sidebar = True
model = Article
def get_side_queryset(self):
def get_sidebar_queryset(self):
qs = Article.objects.select_related('cover') \
.filter(is_static=False) \
.order_by('-date')
@ -27,9 +27,7 @@ class ArticleListView(ParentMixin, PageListView):
template_name = 'aircox/article_list.html'
show_headline = True
is_static = False
parent_model = Program
fk_parent = 'program'
def get_queryset(self):
return super().get_queryset().filter(is_static=self.is_static)

View File

@ -3,6 +3,7 @@ from django.http import Http404
from django.views.generic import DetailView, ListView
from django.views.generic.base import TemplateResponseMixin, ContextMixin
from ..models import Page
from ..utils import Redirect
@ -15,8 +16,10 @@ class BaseView(TemplateResponseMixin, ContextMixin):
cover = None
""" Page cover """
show_side_nav = False
has_sidebar = True
""" Show side navigation """
has_filters = False
""" Show filters nav """
list_count = 5
""" Item count for small lists displayed on page. """
@ -24,27 +27,29 @@ class BaseView(TemplateResponseMixin, ContextMixin):
def station(self):
return self.request.station
def get_queryset(self):
return super().get_queryset().station(self.station)
# def get_queryset(self):
# return super().get_queryset().station(self.station)
def get_side_queryset(self):
def get_sidebar_queryset(self):
""" Return a queryset of items to render on the side nav. """
return None
return Page.objects.select_subclasses().published() \
.order_by('-pub_date')
def get_context_data(self, side_items=None, **kwargs):
def get_context_data(self, sidebar_items=None, **kwargs):
kwargs.setdefault('station', self.station)
kwargs.setdefault('cover', self.cover)
kwargs.setdefault('has_filters', self.has_filters)
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]
has_sidebar = kwargs.setdefault('has_sidebar', self.has_sidebar)
if has_sidebar and sidebar_items is None:
sidebar_items = self.get_sidebar_queryset()
sidebar_items = None if sidebar_items is None else \
sidebar_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(side_items=side_items, **kwargs)
return super().get_context_data(sidebar_items=sidebar_items, **kwargs)

View File

@ -35,18 +35,14 @@ class EpisodeListView(ParentMixin, PageListView):
model = Episode
item_template_name = 'aircox/episode_item.html'
show_headline = True
parent_model = Program
fk_parent = 'program'
class DiffusionListView(GetDateMixin, BaseView, ListView):
""" View for timetables """
model = Diffusion
date = None
start = None
end = None
has_filters = True
redirect_date_url = 'diffusion-list'
def get_date(self):
date = super().get_date()
@ -56,19 +52,7 @@ class DiffusionListView(GetDateMixin, BaseView, ListView):
return super().get_queryset().today(self.date).order_by('start')
def get_context_data(self, **kwargs):
today = datetime.date.today()
start = self.date - datetime.timedelta(days=self.date.weekday())
dates = [
(today, None),
(today - datetime.timedelta(days=1), None),
(today + datetime.timedelta(days=1), None),
(today - datetime.timedelta(days=7), _('next week')),
(today + datetime.timedelta(days=7), _('last week')),
(None, None)
] + [
(date, date.strftime('%A %d'))
for date in (start + datetime.timedelta(days=i)
for i in range(0, 7)) if date != today
]
dates = [start + datetime.timedelta(days=i) for i in range(0, 7)]
return super().get_context_data(date=self.date, dates=dates, **kwargs)

View File

@ -48,6 +48,9 @@ class LogListView(BaseView, LogListMixin, ListView):
Return list of logs for the provided date (from `kwargs` or
`request.GET`, defaults to today).
"""
redirect_date_url = 'log-list'
has_filters = True
def get_date(self):
date, today = super().get_date(), datetime.date.today()
return today if date is None else min(date, today)
@ -56,8 +59,7 @@ class LogListView(BaseView, LogListMixin, ListView):
today = datetime.date.today()
kwargs.update({
'date': self.date,
'dates': ((today - datetime.timedelta(days=i), None)
for i in range(0, 7)),
'dates': (today - datetime.timedelta(days=i) for i in range(0, 7)),
'object_list': self.get_object_list(self.object_list),
})
return super().get_context_data(**kwargs)

View File

@ -1,4 +1,6 @@
from django.shortcuts import get_object_or_404
import dateutil
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from ..utils import str_to_date
@ -12,13 +14,18 @@ class GetDateMixin:
`kwargs['date']`
"""
date = None
redirect_date_url = None
def get_date(self):
if 'date' in self.request.GET:
return str_to_date(self.request.GET['date'], '-')
return self.kwargs['date'] if 'date' in self.kwargs else None
date = self.request.GET.get('date')
return str_to_date(date, '-') if date is not None else \
self.kwargs['date'] if 'date' in self.kwargs else None
def get(self, *args, **kwargs):
if self.redirect_date_url and self.request.GET.get('date'):
return redirect(self.redirect_date_url,
date=self.request.GET['date'].replace('-', '/'))
self.date = self.get_date()
return super().get(*args, **kwargs)
@ -35,8 +42,6 @@ class ParentMixin:
""" 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 """
@ -54,8 +59,7 @@ class ParentMixin:
def get_queryset(self):
if self.parent is not None:
lookup = {self.fk_parent: self.parent}
return super().get_queryset().filter(**lookup)
return super().get_queryset().filter(parent=self.parent)
return super().get_queryset()
def get_context_data(self, **kwargs):

View File

@ -1,6 +1,5 @@
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext_lazy as _
from django.views.generic import DetailView, ListView
@ -19,9 +18,11 @@ __all__ = ['PageDetailView', 'PageListView']
class PageListView(BaseView, ListView):
template_name = 'aircox/page_list.html'
item_template_name = 'aircox/page_item.html'
has_sidebar = True
has_filters = True
paginate_by = 20
show_headline = True
show_side_nav = True
categories = None
def get(self, *args, **kwargs):
@ -36,7 +37,7 @@ class PageListView(BaseView, ListView):
# (by id)
if self.categories:
qs = qs.filter(category__slug__in=self.categories)
return qs.order_by('-date')
return qs.order_by('-pub_date')
def get_categories_queryset(self):
# TODO: use generic reverse field lookup
@ -56,6 +57,7 @@ class PageListView(BaseView, ListView):
class PageDetailView(BaseView, DetailView):
""" Base view class for pages. """
context_object_name = 'page'
has_filters = False
def get_queryset(self):
return super().get_queryset().select_related('cover', 'category')

View File

@ -1,11 +1,13 @@
from django.db.models import Q
from django.core.exceptions import ObjectDoesNotExist
from django.shortcuts import get_object_or_404
from aircox.models import Episode, Program
from ..models import Episode, Program, Page
from .mixins import ParentMixin
from .page import PageDetailView, PageListView
__all__ = ['ProgramPageDetailView', 'ProgramDetailView']
__all__ = ['ProgramPageDetailView', 'ProgramDetailView', 'ProgramPageListView']
class ProgramPageDetailView(PageDetailView):
@ -13,24 +15,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
has_sidebar = True
list_count = 5
def get_side_queryset(self):
return self.program.episode_set.published().order_by('-date')
def get_sidebar_queryset(self):
return super().get_sidebar_queryset().filter(parent=self.object)
class ProgramPageListView(ParentMixin, PageListView):
model = Page
parent_model = Program
queryset = Page.objects.select_subclasses()
class ProgramDetailView(ProgramPageDetailView):
model = Program
def get_articles_queryset(self):
return self.program.article_set.published().order_by('-date')
def get_context_data(self, **kwargs):
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

@ -24,3 +24,9 @@
margin: 1em 0em;
}
ul.menu-list li {
list-style-type: none;
}

View File

@ -26,7 +26,6 @@ export default {
for(var tag of item.value.split(splitReg))
counts[tag.trim()] = (counts[tag.trim()] || 0) + 1;
this.counts = counts;
console.log('counts', this.counts)
}
},

View File

@ -5,6 +5,7 @@ $body-background-color: $light;
@import "~bulma/bulma";
//-- helpers/modifiers
.is-fullwidth { width: 100%; }
.is-fixed-bottom {
position: fixed;
@ -18,6 +19,14 @@ $body-background-color: $light;
background-color: transparent;
}
.is-opacity-light {
opacity: 0.7;
&:hover {
opacity: 1;
}
}
//-- navbar
.navbar + .container {
margin-top: 1em;
}
@ -30,10 +39,44 @@ a.navbar-item.is-active {
border-bottom: 1px grey solid;
}
.navbar .navbar-dropdown {
z-index: 2000;
.navbar {
.navbar-dropdown {
z-index: 2000;
}
.navbar-split {
margin: 0.2em 0em;
margin-right: 1em;
padding-right: 1em;
border-right: 1px $grey-light solid;
display: inline-block;
}
form {
margin: 0em;
padding: 0em;
}
}
//-- filters
.filters {
margin: 1em 0em;
background-color: transparent;
margin-bottom: 1em;
.title {
padding-right: 2em;
margin-right: 1em;
border-right: 1px $grey-light solid;
font-size: $size-5;
color: $text-light;
font-weight: $weight-light;
}
}
/*
.navbar-brand img {
min-height: 6em;

View File

@ -1,7 +1,7 @@
Django>=2.2.0
djangorestframework>=3.9.4
django-model-utils>=3.2.0
dateutils>=0.6.6
watchdog>=0.8.3
psutil>=5.0.1
tzlocal>=1.4
@ -12,7 +12,6 @@ django-filer>=1.5.0
django-ckeditor>=5.7.1
django-admin-sortable2>=0.7.2
django-content-editor>=1.4.2
django-honeypot>=0.5.0
gunicorn>=19.6.0