work on lists filters + nav items
This commit is contained in:
parent
774c558a36
commit
7860d9f92b
|
@ -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
|
@ -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?");
|
||||
|
||||
/***/ }),
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
||||
|
||||
|
|
|
@ -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 """
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user