work on lists filters + nav items

This commit is contained in:
bkfox 2020-11-08 17:54:49 +01:00
parent 774c558a36
commit 7860d9f92b
13 changed files with 126 additions and 86 deletions

View File

@ -185,12 +185,25 @@ class StaticPage(BasePage):
(ATTACH_TO_EPISODES, _('Episodes list')),
(ATTACH_TO_ARTICLES, _('Articles list')),
)
VIEWS = {
ATTACH_TO_HOME: 'home',
ATTACH_TO_DIFFUSIONS: 'diffusion-list',
ATTACH_TO_LOGS: 'log-list',
ATTACH_TO_PROGRAMS: 'program-list',
ATTACH_TO_EPISODES: 'episode-list',
ATTACH_TO_ARTICLES: 'article-list',
}
attach_to = models.SmallIntegerField(
_('attach to'), choices=ATTACH_TO_CHOICES, blank=True, null=True,
help_text=_('display this page content to related element'),
)
def get_absolute_url(self):
if self.attach_to:
return reverse(self.VIEWS[self.attach_to])
return super().get_absolute_url()
class Comment(models.Model):
page = models.ForeignKey(
@ -216,8 +229,7 @@ class NavItem(models.Model):
text = models.CharField(_('title'), max_length=64)
url = models.CharField(_('url'), max_length=256, blank=True, null=True)
page = models.ForeignKey(StaticPage, models.CASCADE,
verbose_name=_('page'), blank=True, null=True,
limit_choices_to={'attach_to__isnull': True})
verbose_name=_('page'), blank=True, null=True)
class Meta:
verbose_name = _('Menu item')
verbose_name_plural = _('Menu items')

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -163,7 +163,7 @@
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"defaultConfig\", function() { return defaultConfig; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return AppBuilder; });\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm.browser.js\");\n\n\nconst defaultConfig = {\n el: '#app',\n delimiters: ['[[', ']]'],\n\n computed: {\n player() { return window.aircox.player; },\n },\n}\n\n\nclass AppBuilder {\n constructor(config={}) {\n this._config = config;\n this.app = null;\n }\n\n get config() {\n let config = this._config instanceof Function ? this._config() : this._config;\n for(var k of new Set([...Object.keys(config || {}), ...Object.keys(defaultConfig)])) {\n if(!config[k] && defaultConfig[k])\n config[k] = defaultConfig[k]\n else if(config[k] instanceof Object)\n config[k] = {...defaultConfig[k], ...config[k]}\n }\n return config;\n }\n\n set config(value) {\n this._config = value;\n }\n\n destroy() {\n self.app && self.app.$destroy();\n self.app = null;\n }\n\n fetch(url, options) {\n return fetch(url, options).then(response => response.text())\n .then(content => {\n let doc = new DOMParser().parseFromString(content, 'text/html');\n let app = doc.getElementById('app');\n content = app ? app.innerHTML : content;\n return this.load({sync: true, content, title: doc.title, url })\n })\n }\n\n load({async=false,content=null, title=null, url=null}={}) {\n var self = this;\n return new Promise((resolve, reject) => {\n let func = () => {\n try {\n let config = self.config;\n const el = document.querySelector(config.el);\n if(!el)\n return reject(`Error: can't get element ${config.el}`)\n\n if(content)\n el.innerHTML = content\n if(title)\n document.title = title;\n if(url && content)\n history.pushState({ content: content, title: title }, '', url)\n\n this.app = new vue__WEBPACK_IMPORTED_MODULE_0__[\"default\"](config);\n resolve(self.app)\n } catch(error) {\n self.destroy();\n reject(error)\n }};\n async ? window.addEventListener('load', func) : func();\n });\n }\n\n loadFromState(state) {\n return this.load({ content: state.content, title: state.title });\n }\n}\n\n\n\n\n//# sourceURL=webpack:///./assets/public/app.js?");
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"defaultConfig\", function() { return defaultConfig; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return AppBuilder; });\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm.browser.js\");\n\n\nconst defaultConfig = {\n el: '#app',\n delimiters: ['[[', ']]'],\n\n computed: {\n player() { return window.aircox.player; },\n },\n}\n\n\nclass AppBuilder {\n constructor(config={}) {\n this._config = config;\n this.title = null;\n this.app = null;\n }\n\n get config() {\n let config = this._config instanceof Function ? this._config() : this._config;\n for(var k of new Set([...Object.keys(config || {}), ...Object.keys(defaultConfig)])) {\n if(!config[k] && defaultConfig[k])\n config[k] = defaultConfig[k]\n else if(config[k] instanceof Object)\n config[k] = {...defaultConfig[k], ...config[k]}\n }\n return config;\n }\n\n set config(value) {\n this._config = value;\n }\n\n destroy() {\n self.app && self.app.$destroy();\n self.app = null;\n }\n\n fetch(url, options) {\n return fetch(url, options).then(response => response.text())\n .then(content => {\n let doc = new DOMParser().parseFromString(content, 'text/html');\n let app = doc.getElementById('app');\n content = app ? app.innerHTML : content;\n return this.load({sync: true, content, title: doc.title, url })\n })\n }\n\n load({async=false,content=null,title=null}={}) {\n var self = this;\n return new Promise((resolve, reject) => {\n let func = () => {\n try {\n let config = self.config;\n const el = document.querySelector(config.el);\n if(!el)\n return reject(`Error: can't get element ${config.el}`)\n\n if(content)\n el.innerHTML = content\n if(title)\n document.title = title;\n this.app = new vue__WEBPACK_IMPORTED_MODULE_0__[\"default\"](config);\n resolve(self.app)\n } catch(error) {\n self.destroy();\n reject(error)\n }};\n async ? window.addEventListener('load', func) : func();\n });\n }\n\n /// Save application state into browser history\n historySave(url,replace=false) {\n const el = document.querySelector(this.config.el);\n const state = {\n content: el.innerHTML,\n title: document.title,\n };\n\n if(replace)\n history.replaceState(state, '', url)\n else\n history.pushState(state, '', url)\n }\n\n /// Load application from browser history's state\n historyLoad(state) {\n return this.load({ content: state.content, title: state.title });\n }\n}\n\n\n\n\n//# sourceURL=webpack:///./assets/public/app.js?");
/***/ }),

View File

@ -5,7 +5,6 @@ variables.
Usefull context:
- cover: image cover
- site: current website
- has_filters: display filter bar (using block "filters")
- model: view model or displayed `object`'s
- sidebar_object_list: item to display in sidebar
- sidebar_url_name: url name sidebar item complete list
@ -99,17 +98,6 @@ Usefull context:
<section class="content">{{ page.content|safe }}</section>
{% endif %}
{% endblock %}
{# TODO: change block name #}
{% if has_filters %}
{% comment %}Translators: extra toolbar displayed on the top of page lists {% endcomment %}
<nav class="navbar toolbar"
aria-label="{% trans "list filters" %}">
{% block filters %}{% endblock %}
</nav>
{% endif %}
{% endblock main %}
</main>

View File

@ -24,6 +24,49 @@
{% block main %}{{ block.super }}
{% block before_list %}
{% if filters %}
<form method="GET" action="" class="media">
<div class="media-content">
{% block filters %}
{% for label, name, choices in filters %}
<div class="field is-horizontal">
<div class="field-label">
<label class="label">{{ label }}</label>
</div>
<div class="field-body">
<div class="field is-narrow">
<div class="control">
{% for label, value, checked in choices %}
<label class="checkbox">
<input type="checkbox" class="checkbox" name="{{ name }}"
value="{{ value }}"
{% if checked %}checked{% endif %} />
{{ label }}
</label>
{% endfor %}
</div>
</div>
</div>
</div>
{% endfor %}
{% endblock %}
</div>
<div class="media-right">
<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>
{% endif %}
{% endblock %}
<section role="list">
{% block pages_list %}
{% with has_headline=True %}

View File

@ -14,7 +14,7 @@
{% block subtitle %}{{ date|date:"l d F Y" }}{% endblock %}
{% block filters %}
{% block before_list %}
{% with "diffusion-list" as url_name %}
{% include "aircox/widgets/dates_menu.html" %}
{% endwith %}

View File

@ -15,7 +15,7 @@
{% block subtitle %}{{ date|date:"l d F Y" }}{% endblock %}
{% block filters %}
{% block before_list %}
{% with "log-list" as url_name %}
{% include "aircox/widgets/dates_menu.html" %}
{% endwith %}

View File

@ -2,49 +2,3 @@
{% comment %}Display a list of Pages{% endcomment %}
{% load i18n aircox %}
{% block filters %}
{# FIXME #}
{% if filter_categories %}
<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-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>
</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>
</div>
</form>
{% endif %}
{% endblock %}

View File

@ -1,6 +1,7 @@
from collections import OrderedDict
import datetime
from django.utils.translation import gettext_lazy as _
from django.views.generic import ListView
from ..models import Diffusion, Episode, Program, StaticPage, Sound
@ -29,6 +30,17 @@ class EpisodeListView(PageListView):
parent_model = Program
attach_to_value = StaticPage.ATTACH_TO_EPISODES
def get_queryset(self):
qs = super().get_queryset()
if self.filters and 'podcasts' in self.filters:
qs = qs.filter(sound__is_public=True)
return qs
def get_filters(self):
return super().get_filters() + (
(_('Podcasts'), 'podcasts', tuple()),
)
class DiffusionListView(GetDateMixin, AttachedToMixin, BaseView, ListView):
""" View for timetables """

View File

@ -76,9 +76,11 @@ class PageListView(BasePageListView):
template_name = None
has_filters = True
categories = None
filters = None
def get(self, *args, **kwargs):
self.categories = set(self.request.GET.getlist('categories'))
self.filters = set(self.request.GET.getlist('filters'))
return super().get(*args, **kwargs)
def get_template_names(self):
@ -94,16 +96,25 @@ class PageListView(BasePageListView):
qs = qs.filter(category__slug__in=self.categories)
return qs
def get_categories_queryset(self):
# TODO: use generic reverse field lookup
def get_filters(self):
categories = self.model.objects.published() \
.filter(category__isnull=False) \
.values_list('category', flat=True)
return Category.objects.filter(id__in=categories)
categories = [ (c.title, c.slug, c.slug in self.categories)
for c in Category.objects.filter(id__in=categories) ]
return (
(_('Categories'), 'categories', categories),
)
def get_context_data(self, **kwargs):
kwargs.setdefault('filter_categories', self.get_categories_queryset())
kwargs.setdefault('categories', self.categories)
if not 'filters' in kwargs:
filters = self.get_filters()
for label, fieldName, choices in filters:
if choices:
kwargs['filters'] = filters
break;
else:
kwargs['filters'] = tuple()
return super().get_context_data(**kwargs)

View File

@ -13,6 +13,7 @@ export const defaultConfig = {
export default class AppBuilder {
constructor(config={}) {
this._config = config;
this.title = null;
this.app = null;
}
@ -46,7 +47,7 @@ export default class AppBuilder {
})
}
load({async=false,content=null, title=null, url=null}={}) {
load({async=false,content=null,title=null}={}) {
var self = this;
return new Promise((resolve, reject) => {
let func = () => {
@ -60,9 +61,6 @@ export default class AppBuilder {
el.innerHTML = content
if(title)
document.title = title;
if(url && content)
history.pushState({ content: content, title: title }, '', url)
this.app = new Vue(config);
resolve(self.app)
} catch(error) {
@ -73,7 +71,22 @@ export default class AppBuilder {
});
}
loadFromState(state) {
/// Save application state into browser history
historySave(url,replace=false) {
const el = document.querySelector(this.config.el);
const state = {
content: el.innerHTML,
title: document.title,
};
if(replace)
history.replaceState(state, '', url)
else
history.pushState(state, '', url)
}
/// Load application from browser history's state
historyLoad(state) {
return this.load({ content: state.content, title: state.title });
}
}

View File

@ -40,6 +40,7 @@ window.aircox = {
get playerApp() { return this.playerBuilder && this.playerBuilder.app },
get player() { return this.playerApp && this.playerApp.$refs.player },
// Handle hot-reload (link click and form submits).
onPageFetch(event) {
let submit = event.type == 'submit';
let target = submit || event.target.tagName == 'A'
@ -62,7 +63,9 @@ window.aircox = {
options['body'] = formData;
}
}
this.appBuilder.fetch(url, options);
this.appBuilder.fetch(url, options).then(app => {
this.appBuilder.historySave(url);
});
event.preventDefault();
event.stopPropagation();
},
@ -76,12 +79,16 @@ aircox.playerBuilder = new AppBuilder({el: '#player'});
aircox.playerBuilder.load({async:true});
aircox.appBuilder = new AppBuilder(x => window.aircox.appConfig);
aircox.appBuilder.load({async:true}).then(app => {
aircox.appBuilder.historySave(document.location, true);
//-- load page hooks
window.addEventListener('click', event => aircox.onPageFetch(event), true);
window.addEventListener('submit', event => aircox.onPageFetch(event), true);
window.addEventListener('popstate', event => {
if(event.state && event.state.content)
aircox.appBuilder.loadFromState(event.state);
if(event.state && event.state.content) {
document.title = aircox.appBuilder.title;
aircox.appBuilder.historyLoad(event.state);
}
});
})