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) @admin.register(Article)
class ArticleAdmin(PageAdmin): class ArticleAdmin(PageAdmin):
list_display = PageAdmin.list_display + ('program',) list_filter = PageAdmin.list_filter
list_filter = PageAdmin.list_filter + ('program',) search_fields = PageAdmin.search_fields + ['parent__title']
search_fields = PageAdmin.search_fields + ['program__title']
# TODO: readonly field # 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) @admin.register(Episode)
class EpisodeAdmin(PageAdmin): class EpisodeAdmin(PageAdmin):
list_display = PageAdmin.list_display + ('program',) list_display = PageAdmin.list_display
list_filter = PageAdmin.list_filter + ('program',) list_filter = PageAdmin.list_filter
search_fields = PageAdmin.search_fields + ['program__title'] search_fields = PageAdmin.search_fields + ['parent__title']
readonly_fields = ('program',) # readonly_fields = ('parent',)
fieldsets = copy.deepcopy(PageAdmin.fieldsets)
fieldsets[1][1]['fields'].insert(0, 'program')
inlines = [TracksInline, SoundInline, DiffusionInline] inlines = [TracksInline, SoundInline, DiffusionInline]

View File

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

View File

@ -33,7 +33,8 @@ class WeekConverter:
return datetime.datetime.strptime(value + '/1', '%G/%V/%u').date() return datetime.datetime.strptime(value + '/1', '%G/%V/%u').date()
def to_url(self, value): 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: class DateConverter:
@ -41,8 +42,9 @@ class DateConverter:
regex = r'[0-9]{4}/[0-9]{2}/[0-9]{2}' regex = r'[0-9]{4}/[0-9]{2}/[0-9]{2}'
def to_python(self, value): 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): def to_url(self, value):
return '{:04d}/{:02d}/{:02d}'.format(value.year, value.month, return value if isinstance(value, str) else \
value.day) '{: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 django.utils.translation import ugettext_lazy as _
from .page import Page, PageQuerySet from .page import Page, PageQuerySet
from .program import Program, InProgramQuerySet from .program import Program, ProgramChildQuerySet
class ArticleQuerySet(InProgramQuerySet, PageQuerySet):
pass
class Article(Page): class Article(Page):
detail_url_name = 'article-detail' 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 = models.BooleanField(
_('is static'), default=False, _('is static'), default=False,
help_text=_('Should this article be considered as a page ' help_text=_('Should this article be considered as a page '
'instead of a blog article'), 'instead of a blog article'),
) )
objects = ArticleQuerySet.as_manager() objects = ProgramChildQuerySet.as_manager()
class Meta: class Meta:
verbose_name = _('Article') verbose_name = _('Article')

View File

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

View File

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

View File

@ -23,7 +23,8 @@ from .station import Station
logger = logging.getLogger('aircox') logger = logging.getLogger('aircox')
__all__ = ['Program', 'ProgramQuerySet', 'Stream', 'Schedule'] __all__ = ['Program', 'ProgramQuerySet', 'Stream', 'Schedule',
'ProgramChildQuerySet', 'BaseRerun', 'BaseRerunQuerySet']
class ProgramQuerySet(PageQuerySet): class ProgramQuerySet(PageQuerySet):
@ -49,15 +50,8 @@ class Program(Page):
name if it does not exists. name if it does not exists.
""" """
# explicit foreign key in order to avoid related name clashes # explicit foreign key in order to avoid related name clashes
page = models.OneToOneField( station = models.ForeignKey(Station, models.CASCADE,
Page, models.CASCADE, verbose_name=_('station'))
parent_link=True, related_name='program_page'
)
station = models.ForeignKey(
Station,
verbose_name=_('station'),
on_delete=models.CASCADE,
)
active = models.BooleanField( active = models.BooleanField(
_('active'), _('active'),
default=True, default=True,
@ -146,10 +140,17 @@ class Program(Page):
.update(path=Concat('path', Substr(F('path'), len(path_)))) .update(path=Concat('path', Substr(F('path'), len(path_))))
class InProgramQuerySet(models.QuerySet): class ProgramChildQuerySet(PageQuerySet):
""" def station(self, station=None, id=None):
Queryset for model having a ForeignKey field "program" to `Program`. 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): def station(self, station=None, id=None):
return self.filter(program__station=station) if id is None else \ return self.filter(program__station=station) if id is None else \
self.filter(program__station__id=id) self.filter(program__station__id=id)
@ -158,9 +159,6 @@ class InProgramQuerySet(models.QuerySet):
return self.filter(program=program) if id is None else \ return self.filter(program=program) if id is None else \
self.filter(program__id=id) self.filter(program__id=id)
class BaseRerunQuerySet(InProgramQuerySet):
""" Queryset for BaseRerun (sub)classes. """
def rerun(self): def rerun(self):
return self.filter(initial__isnull=False) 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) @receiver(signals.post_delete, sender=Diffusion)
def diffusion_post_delete(sender, instance, *args, **kwargs): 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) \ sound__isnull=True) \
.delete() .delete()

View File

@ -16,6 +16,9 @@
width: 100%; width: 100%;
margin: 1em 0em; } margin: 1em 0em; }
ul.menu-list li {
list-style-type: none; }
@keyframes spinAround { @keyframes spinAround {
from { from {
transform: rotate(0deg); } transform: rotate(0deg); }
@ -7180,6 +7183,11 @@ label.panel-block {
.has-background-transparent { .has-background-transparent {
background-color: transparent; } background-color: transparent; }
.is-opacity-light {
opacity: 0.7; }
.is-opacity-light:hover {
opacity: 1; }
.navbar + .container { .navbar + .container {
margin-top: 1em; } margin-top: 1em; }
@ -7192,6 +7200,29 @@ a.navbar-item.is-active {
.navbar .navbar-dropdown { .navbar .navbar-dropdown {
z-index: 2000; } 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 { .navbar-brand img {
min-height: 6em; 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__) { /***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict"; "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 { .has-background-transparent {
background-color: transparent; } background-color: transparent; }
.is-opacity-light {
opacity: 0.7; }
.is-opacity-light:hover {
opacity: 1; }
.navbar + .container { .navbar + .container {
margin-top: 1em; } margin-top: 1em; }
@ -7174,6 +7179,29 @@ a.navbar-item.is-active {
.navbar .navbar-dropdown { .navbar .navbar-dropdown {
z-index: 2000; } 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 { .navbar-brand img {
min-height: 6em; min-height: 6em;

View File

@ -4,7 +4,9 @@
{% block content %}{{ block.super }} {% block content %}{{ block.super }}
{# TODO: date subtitle #} {# TODO: date subtitle #}
<a-statistics> <div class="columns">
<a-statistics class="column">
<template v-slot:default="{counts}"> <template v-slot:default="{counts}">
<table class="table is-hoverable is-fullwidth"> <table class="table is-hoverable is-fullwidth">
<thead> <thead>
@ -63,6 +65,15 @@
</template> </template>
</a-statistics> </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 %} {% endblock %}

View File

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

View File

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

View File

@ -3,6 +3,7 @@
Context: Context:
- cover: image cover - cover: image cover
- site: current website - site: current website
- has_filters: display filter bar (using block "filters")
{% endcomment %} {% endcomment %}
<html> <html>
<head> <head>
@ -71,19 +72,50 @@ Context:
{% endblock %} {% endblock %}
</header> </header>
{% if has_filters %}
<nav class="navbar filters"
aria-label="{% trans "list filters" %}">
{% block filters %}{% endblock %}
</nav>
{% endif %}
{% block main %}{% endblock main %} {% block main %}{% endblock main %}
</main> </main>
{% if show_side_nav %} {% if has_sidebar %}
<aside class="column is-one-third-desktop"> <aside class="column is-one-third-desktop">
{# FIXME: block cover into side_nav one #} {# FIXME: block cover into sidebar one #}
{% block cover %} {% block cover %}
{% if cover is not None %} {% if cover is not None %}
<img class="cover" src="{{ cover.url }}" class="cover"/> <img class="cover" src="{{ cover.url }}" class="cover"/>
{% endif %} {% endif %}
{% endblock %} {% 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 %} {% endblock %}
</aside> </aside>
{% endif %} {% 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" %} {% extends "aircox/page.html" %}
{% load i18n aircox %} {% load i18n aircox humanize %}
{% block title %} {% block title %}
{% with station.name as station %} {% with station.name as station %}
@ -9,17 +9,22 @@
{% block subtitle %}{{ date|date:"l d F Y" }}{% endblock %} {% block subtitle %}{{ date|date:"l d F Y" }}{% endblock %}
{% block main %}{{ block.super }} {% block filters %}
<div class="columns"> {% with "diffusion-list" as url_name %}
{% include "aircox/widgets/dates_menu.html" %}
{% endwith %}
{% endblock %}
{% block main %}{{ block.super }}
{% with True as hide_schedule %} {% with True as hide_schedule %}
<section class="column"> <section role="list">
<div id="timetable-{{ date|date:"Y-m-d" }}"> <div id="timetable-{{ date|date:"Y-m-d" }}">
{% for diffusion in object_list %} {% 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"> <div class="column is-one-fifth has-text-right">
<time datetime="{{ diffusion.start|date:"c" }}"> <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> </time>
</div> </div>
<div class="column"> <div class="column">
@ -33,12 +38,10 @@
</section> </section>
{% endwith %} {% endwith %}
{% comment %}
<nav class="column menu is-one-third-desktop" role="menu"> <nav class="column menu is-one-third-desktop" role="menu">
{% with "diffusion-list" as url_name %}
{% include "aircox/widgets/dates_menu.html" %}
{% endwith %}
</nav> </nav>
{% endcomment %}
</div>
{% endblock %} {% endblock %}

View File

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

View File

@ -10,14 +10,17 @@
{% block subtitle %}{{ date|date:"l d F Y" }}{% endblock %} {% 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 %} {% block main %}
<div class="columns"> <section>
<section class="section column">
{# <h4 class="subtitle size-4">{{ date }}</h4> #} {# <h4 class="subtitle size-4">{{ date }}</h4> #}
{% with True as hide_schedule %} {% with True as hide_schedule %}
<table class="table is-striped is-hoverable is-fullwidth has-background-transparent"> <table class="table is-striped is-hoverable is-fullwidth">
{% for object in object_list %} {% for object in object_list %}
<tr> <tr>
<td> <td>
@ -37,13 +40,5 @@
</table> </table>
{% endwith %} {% endwith %}
</section> </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 %} {% endblock %}

View File

@ -2,16 +2,21 @@
{% load i18n aircox %} {% load i18n aircox %}
{% block title %} {% 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 %} {% endblock %}
{% block side_nav %} {% block filters %}
{{ block.super }} <div class="navbar-branding">
<h4 class="navbar-item title">{% trans "Filters" %}</h4>
{% if filter_categories|length != 1 %} </div>
<section class="toolbar"> <form method="GET" action="" class="navbar-menu">
<h4 class="subtitle is-5">{% trans "Filters" %}</h4> <div class="navbar-start">
<form method="GET" action=""> <div class="navbar-item">
{% block list_filters %} {% block list_filters %}
<div class="field is-horizontal"> <div class="field is-horizontal">
<div class="field-label"> <div class="field-label">
@ -33,11 +38,10 @@
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
<div class="field is-horizontal">
<div class="field-label">
<label class="label"></label>
</div> </div>
<div class="field-body"> </div>
<div class="navbar-end">
<div class="navbar-item">
<div class="field is-grouped is-grouped-right"> <div class="field is-grouped is-grouped-right">
<div class="control"> <div class="control">
<button class="button is-primary"/>{% trans "Apply" %}</button> <button class="button is-primary"/>{% trans "Apply" %}</button>
@ -49,16 +53,14 @@
</div> </div>
</div> </div>
</form> </form>
</section>
{% endif %}
{% endblock %} {% endblock %}
{% block main %} {% block main %}
<section> <section role="list">
{% for object in object_list %} {% for object in object_list %}
{% block list_object %} {% block list_object %}
{% include item_template_name %} {% include object.item_template_name|default:item_template_name %}
{% endblock %} {% endblock %}
{% endfor %} {% endfor %}
</section> </section>

View File

@ -1,30 +1,16 @@
{% extends "aircox/page_detail.html" %} {% extends "aircox/page_detail.html" %}
{% load i18n %} {% load i18n %}
{% block side_nav %} {% block sidebar_title %}
{{ block.super }} {% with program.title as program %}
{% blocktrans %} Recently on {{ program }}{% endblocktrans %}
{% if side_items %} {% endwith %}
<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 %}
{% endblock %} {% endblock %}
{% block sidebar %}
{% with program as parent %}
{{ block.super }}
{% endwith %}
{% endblock %}

View File

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

View File

@ -9,12 +9,21 @@ Context:
An empty date results to a title or a separator An empty date results to a title or a separator
{% endcomment %} {% 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" %}"> aria-label="{% trans "pick a date" %}">
<p class="menu-label">{% trans "Pick a date" %}</p> <div class="navbar-start">
<form action="{% url url_name %}" method="GET" {% 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 %}
</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" %}"> aria-label="{% trans "Jump to date" %}">
<div class="field has-addons"> <div class="field has-addons">
<div class="control has-icons-left"> <div class="control has-icons-left">
@ -27,22 +36,8 @@ An empty date results to a title or a separator
</div> </div>
</div> </div>
</form> </form>
</div>
<ul class="menu-list"> </div>
{% for day, title in dates %} </div>
{% 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 %}
{% endfor %}
</ul>
</nav>

View File

@ -24,6 +24,8 @@ api = [
urls = [ urls = [
path(_(''),
views.DiffusionListView.as_view(), name='home'),
path('api/', include(api)), path('api/', include(api)),
# path('', views.PageDetailView.as_view(model=models.Article), # path('', views.PageDetailView.as_view(model=models.Article),
@ -35,15 +37,6 @@ urls = [
views.ArticleDetailView.as_view(), views.ArticleDetailView.as_view(),
name='article-detail'), 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/'), path(_('episodes/'),
views.EpisodeListView.as_view(), name='episode-list'), views.EpisodeListView.as_view(), name='episode-list'),
path(_('episodes/<slug:slug>/'), path(_('episodes/<slug:slug>/'),
@ -53,7 +46,23 @@ urls = [
path(_('week/<date:date>/'), path(_('week/<date:date>/'),
views.DiffusionListView.as_view(), name='diffusion-list'), views.DiffusionListView.as_view(), name='diffusion-list'),
path(_('logs/'), views.LogListView.as_view(), name='logs'), path(_('logs/'), views.LogListView.as_view(), name='log-list'),
path(_('logs/<date:date>/'), views.LogListView.as_view(), name='logs'), path(_('logs/<date:date>/'), views.LogListView.as_view(), name='log-list'),
# path('<page_path:path>', views.route_page, name='page'), # 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 .episode import EpisodeDetailView, EpisodeListView, DiffusionListView
from .log import LogListView from .log import LogListView
from .page import PageDetailView, PageListView 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/', path('tools/statistics/',
self.admin_view(StatisticsView.as_view()), self.admin_view(StatisticsView.as_view()),
name='tools-stats'), name='tools-stats'),
path('tools/statistics/<date:date>/',
self.admin_view(StatisticsView.as_view()),
name='tools-stats'),
] ]
return urls return urls

View File

@ -7,10 +7,10 @@ __all__ = ['ArticleDetailView', 'ArticleListView']
class ArticleDetailView(PageDetailView): class ArticleDetailView(PageDetailView):
show_side_nav = True has_sidebar = True
model = Article model = Article
def get_side_queryset(self): def get_sidebar_queryset(self):
qs = Article.objects.select_related('cover') \ qs = Article.objects.select_related('cover') \
.filter(is_static=False) \ .filter(is_static=False) \
.order_by('-date') .order_by('-date')
@ -27,9 +27,7 @@ class ArticleListView(ParentMixin, PageListView):
template_name = 'aircox/article_list.html' template_name = 'aircox/article_list.html'
show_headline = True show_headline = True
is_static = False is_static = False
parent_model = Program parent_model = Program
fk_parent = 'program'
def get_queryset(self): def get_queryset(self):
return super().get_queryset().filter(is_static=self.is_static) 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 import DetailView, ListView
from django.views.generic.base import TemplateResponseMixin, ContextMixin from django.views.generic.base import TemplateResponseMixin, ContextMixin
from ..models import Page
from ..utils import Redirect from ..utils import Redirect
@ -15,8 +16,10 @@ class BaseView(TemplateResponseMixin, ContextMixin):
cover = None cover = None
""" Page cover """ """ Page cover """
show_side_nav = False has_sidebar = True
""" Show side navigation """ """ Show side navigation """
has_filters = False
""" Show filters nav """
list_count = 5 list_count = 5
""" Item count for small lists displayed on page. """ """ Item count for small lists displayed on page. """
@ -24,27 +27,29 @@ class BaseView(TemplateResponseMixin, ContextMixin):
def station(self): def station(self):
return self.request.station return self.request.station
def get_queryset(self): # def get_queryset(self):
return super().get_queryset().station(self.station) # 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 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('station', self.station)
kwargs.setdefault('cover', self.cover) kwargs.setdefault('cover', self.cover)
kwargs.setdefault('has_filters', self.has_filters)
show_side_nav = kwargs.setdefault('show_side_nav', self.show_side_nav) has_sidebar = kwargs.setdefault('has_sidebar', self.has_sidebar)
if show_side_nav and side_items is None: if has_sidebar and sidebar_items is None:
side_items = self.get_side_queryset() sidebar_items = self.get_sidebar_queryset()
side_items = None if side_items is None else \ sidebar_items = None if sidebar_items is None else \
side_items[:self.list_count] sidebar_items[:self.list_count]
if not 'audio_streams' in kwargs: if not 'audio_streams' in kwargs:
streams = self.station.audio_streams streams = self.station.audio_streams
streams = streams and streams.split('\n') streams = streams and streams.split('\n')
kwargs['audio_streams'] = streams kwargs['audio_streams'] = streams
return super().get_context_data(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 model = Episode
item_template_name = 'aircox/episode_item.html' item_template_name = 'aircox/episode_item.html'
show_headline = True show_headline = True
parent_model = Program parent_model = Program
fk_parent = 'program'
class DiffusionListView(GetDateMixin, BaseView, ListView): class DiffusionListView(GetDateMixin, BaseView, ListView):
""" View for timetables """ """ View for timetables """
model = Diffusion model = Diffusion
has_filters = True
date = None redirect_date_url = 'diffusion-list'
start = None
end = None
def get_date(self): def get_date(self):
date = super().get_date() date = super().get_date()
@ -56,19 +52,7 @@ class DiffusionListView(GetDateMixin, BaseView, ListView):
return super().get_queryset().today(self.date).order_by('start') return super().get_queryset().today(self.date).order_by('start')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
today = datetime.date.today()
start = self.date - datetime.timedelta(days=self.date.weekday()) start = self.date - datetime.timedelta(days=self.date.weekday())
dates = [ dates = [start + datetime.timedelta(days=i) for i in range(0, 7)]
(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
]
return super().get_context_data(date=self.date, dates=dates, **kwargs) 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 Return list of logs for the provided date (from `kwargs` or
`request.GET`, defaults to today). `request.GET`, defaults to today).
""" """
redirect_date_url = 'log-list'
has_filters = True
def get_date(self): def get_date(self):
date, today = super().get_date(), datetime.date.today() date, today = super().get_date(), datetime.date.today()
return today if date is None else min(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() today = datetime.date.today()
kwargs.update({ kwargs.update({
'date': self.date, 'date': self.date,
'dates': ((today - datetime.timedelta(days=i), None) 'dates': (today - datetime.timedelta(days=i) for i in range(0, 7)),
for i in range(0, 7)),
'object_list': self.get_object_list(self.object_list), 'object_list': self.get_object_list(self.object_list),
}) })
return super().get_context_data(**kwargs) 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 from ..utils import str_to_date
@ -12,13 +14,18 @@ class GetDateMixin:
`kwargs['date']` `kwargs['date']`
""" """
date = None date = None
redirect_date_url = None
def get_date(self): def get_date(self):
if 'date' in self.request.GET: date = self.request.GET.get('date')
return str_to_date(self.request.GET['date'], '-') return str_to_date(date, '-') if date is not None else \
return self.kwargs['date'] if 'date' in self.kwargs else None self.kwargs['date'] if 'date' in self.kwargs else None
def get(self, *args, **kwargs): 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() self.date = self.get_date()
return super().get(*args, **kwargs) return super().get(*args, **kwargs)
@ -35,8 +42,6 @@ class ParentMixin:
""" Url lookup argument """ """ Url lookup argument """
parent_field = 'slug' parent_field = 'slug'
""" Parent field for url lookup """ """ Parent field for url lookup """
fk_parent = 'page'
""" Page foreign key to the parent """
parent = None parent = None
""" Parent page object """ """ Parent page object """
@ -54,8 +59,7 @@ class ParentMixin:
def get_queryset(self): def get_queryset(self):
if self.parent is not None: if self.parent is not None:
lookup = {self.fk_parent: self.parent} return super().get_queryset().filter(parent=self.parent)
return super().get_queryset().filter(**lookup)
return super().get_queryset() return super().get_queryset()
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):

View File

@ -1,6 +1,5 @@
from django.http import Http404, HttpResponse from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views.generic import DetailView, ListView from django.views.generic import DetailView, ListView
@ -19,9 +18,11 @@ __all__ = ['PageDetailView', 'PageListView']
class PageListView(BaseView, ListView): class PageListView(BaseView, ListView):
template_name = 'aircox/page_list.html' template_name = 'aircox/page_list.html'
item_template_name = 'aircox/page_item.html' item_template_name = 'aircox/page_item.html'
has_sidebar = True
has_filters = True
paginate_by = 20 paginate_by = 20
show_headline = True show_headline = True
show_side_nav = True
categories = None categories = None
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
@ -36,7 +37,7 @@ class PageListView(BaseView, ListView):
# (by id) # (by id)
if self.categories: if self.categories:
qs = qs.filter(category__slug__in=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): def get_categories_queryset(self):
# TODO: use generic reverse field lookup # TODO: use generic reverse field lookup
@ -56,6 +57,7 @@ class PageListView(BaseView, ListView):
class PageDetailView(BaseView, DetailView): class PageDetailView(BaseView, DetailView):
""" Base view class for pages. """ """ Base view class for pages. """
context_object_name = 'page' context_object_name = 'page'
has_filters = False
def get_queryset(self): def get_queryset(self):
return super().get_queryset().select_related('cover', 'category') 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.core.exceptions import ObjectDoesNotExist
from django.shortcuts import get_object_or_404 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 from .page import PageDetailView, PageListView
__all__ = ['ProgramPageDetailView', 'ProgramDetailView'] __all__ = ['ProgramPageDetailView', 'ProgramDetailView', 'ProgramPageListView']
class ProgramPageDetailView(PageDetailView): 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. Base view class for a page that is displayed as a program's child page.
""" """
program = None program = None
show_side_nav = True has_sidebar = True
list_count = 5 list_count = 5
def get_side_queryset(self): def get_sidebar_queryset(self):
return self.program.episode_set.published().order_by('-date') 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): class ProgramDetailView(ProgramPageDetailView):
model = Program model = Program
def get_articles_queryset(self):
return self.program.article_set.published().order_by('-date')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
self.program = kwargs.setdefault('program', self.object) self.program = kwargs.setdefault('program', self.object)
if 'articles' not in kwargs:
kwargs['articles'] = \
self.get_articles_queryset()[:self.list_count]
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)

View File

@ -24,3 +24,9 @@
margin: 1em 0em; 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)) for(var tag of item.value.split(splitReg))
counts[tag.trim()] = (counts[tag.trim()] || 0) + 1; counts[tag.trim()] = (counts[tag.trim()] || 0) + 1;
this.counts = counts; this.counts = counts;
console.log('counts', this.counts)
} }
}, },

View File

@ -5,6 +5,7 @@ $body-background-color: $light;
@import "~bulma/bulma"; @import "~bulma/bulma";
//-- helpers/modifiers
.is-fullwidth { width: 100%; } .is-fullwidth { width: 100%; }
.is-fixed-bottom { .is-fixed-bottom {
position: fixed; position: fixed;
@ -18,6 +19,14 @@ $body-background-color: $light;
background-color: transparent; background-color: transparent;
} }
.is-opacity-light {
opacity: 0.7;
&:hover {
opacity: 1;
}
}
//-- navbar
.navbar + .container { .navbar + .container {
margin-top: 1em; margin-top: 1em;
} }
@ -30,10 +39,44 @@ a.navbar-item.is-active {
border-bottom: 1px grey solid; border-bottom: 1px grey solid;
} }
.navbar .navbar-dropdown { .navbar {
.navbar-dropdown {
z-index: 2000; 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 { .navbar-brand img {
min-height: 6em; min-height: 6em;

View File

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