navigation & breadcrumbs

This commit is contained in:
bkfox 2023-12-12 20:07:58 +01:00
parent eaa1e2412a
commit 46a9008cda
29 changed files with 454 additions and 347 deletions

View File

@ -18,6 +18,7 @@ class EpisodeQuerySet(ProgramChildQuerySet):
class Episode(Page):
objects = EpisodeQuerySet.as_manager()
detail_url_name = "episode-detail"
list_url_name = "episode-list"
template_prefix = "episode"
@property

View File

@ -139,7 +139,9 @@ class BasePage(Renderable, models.Model):
super().save(*args, **kwargs)
def get_absolute_url(self):
return reverse(self.detail_url_name, kwargs={"slug": self.slug}) if self.is_published else "#"
if self.is_published:
return reverse(self.detail_url_name, kwargs={"slug": self.slug})
return ""
@property
def is_draft(self):
@ -216,6 +218,8 @@ class Page(BasePage):
)
objects = PageQuerySet.as_manager()
detail_url_name = ""
list_url_name = "page-list"
@cached_property
def parent_subclass(self):
@ -223,6 +227,15 @@ class Page(BasePage):
return Page.objects.get_subclass(id=self.parent_id)
return None
def get_absolute_url(self):
if not self.is_published and self.parent_subclass:
return self.parent_subclass.get_absolute_url()
return super().get_absolute_url()
@classmethod
def get_list_url(cls, kwargs={}):
return reverse(cls.list_url_name, kwargs=kwargs)
class Meta:
verbose_name = _("Publication")
verbose_name_plural = _("Publications")
@ -243,46 +256,32 @@ class StaticPage(BasePage):
detail_url_name = "static-page-detail"
ATTACH_TO_HOME = 0x00
ATTACH_TO_DIFFUSIONS = 0x01
ATTACH_TO_LOGS = 0x02
ATTACH_TO_PROGRAMS = 0x03
ATTACH_TO_EPISODES = 0x04
ATTACH_TO_ARTICLES = 0x05
ATTACH_TO_PAGES = 0x06
ATTACH_TO_PODCASTS = 0x07
class Target(models.TextChoices):
HOME = "", _("Home Page")
TIMETABLE = "timetable-list", _("Timetable")
PROGRAMS = "program-list", _("Programs list")
EPISODES = "episode-list", _("Episodes list")
ARTICLES = "article-list", _("Articles list")
PAGES = "page-list", _("Publications list")
PODCASTS = "podcast-list", _("Podcasts list")
ATTACH_TO_CHOICES = (
(ATTACH_TO_HOME, _("Home page")),
(ATTACH_TO_DIFFUSIONS, _("Diffusions page")),
(ATTACH_TO_LOGS, _("Logs page")),
(ATTACH_TO_PROGRAMS, _("Programs list")),
(ATTACH_TO_EPISODES, _("Episodes list")),
(ATTACH_TO_ARTICLES, _("Articles list")),
(ATTACH_TO_PAGES, _("Publications list")),
(ATTACH_TO_PODCASTS, _("Podcasts 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_PAGES: "page-list",
}
attach_to = models.SmallIntegerField(
attach_to = models.CharField(
_("attach to"),
choices=ATTACH_TO_CHOICES,
choices=Target.choices,
max_length=32,
blank=True,
null=True,
help_text=_("display this page content to related element"),
)
def get_related_view(self):
from ..views import attached
return self.attach_to and attached.get(self.attach_to) or None
def get_absolute_url(self):
if self.attach_to:
return reverse(self.VIEWS[self.attach_to])
return reverse(self.attach_to)
return super().get_absolute_url()
@ -320,7 +319,7 @@ class NavItem(models.Model):
station = models.ForeignKey(Station, models.CASCADE, verbose_name=_("station"))
menu = models.SlugField(_("menu"), max_length=24)
order = models.PositiveSmallIntegerField(_("order"))
text = models.CharField(_("title"), max_length=64)
text = models.CharField(_("title"), max_length=64, blank=True, null=True)
url = models.CharField(_("url"), max_length=256, blank=True, null=True)
page = models.ForeignKey(
StaticPage,
@ -339,14 +338,21 @@ class NavItem(models.Model):
def get_url(self):
return self.url if self.url else self.page.get_absolute_url() if self.page else None
def get_label(self):
if self.text:
return self.text
elif self.page:
return self.page.title
def render(self, request, css_class="", active_class=""):
url = self.get_url()
text = self.get_text()
if active_class and request.path.startswith(url):
css_class += " " + active_class
if not url:
return self.text
return text
elif not css_class:
return format_html('<a href="{}">{}</a>', url, self.text)
return format_html('<a href="{}">{}</a>', url, text)
else:
return format_html('<a href="{}" class="{}">{}</a>', url, css_class, self.text)
return format_html('<a href="{}" class="{}">{}</a>', url, css_class, text)

View File

@ -61,6 +61,7 @@ class Program(Page):
objects = ProgramQuerySet.as_manager()
detail_url_name = "program-detail"
list_url_name = "program-list"
@property
def path(self):

View File

@ -33,9 +33,9 @@
--a-player-panel-bg: var(--highlight-color);
--a-player-bar-bg: var(--highlight-color);
--a-player-bar-title-alone-sz: 1.6rem;
--button-fg: var(--text-color);
--button-fg: var(--highlight-color-2);
--button-bg: var(--highlight-color);
--button-hg-fg: var(--highlight-color-2);
--button-hg-fg: var(--text-color);
--button-hg-bg: var(--highlight-color);
--button-active-fg: var(--highlight-color);
--button-active-bg: var(--highlight-color-2);
@ -234,6 +234,14 @@
height: var(--preview-cover-size);
width: var(--preview-cover-size);
}
.preview-card.small {
height: var(--preview-cover-small-size);
width: var(--preview-cover-small-size);
}
.preview-card.tiny {
height: var(--preview-cover-tiny-size);
width: var(--preview-cover-tiny-size);
}
.preview-card:not(.header) {
box-shadow: 0em 0em 1em rgba(0, 0, 0, 0.2);
}
@ -259,57 +267,6 @@ preview-header:not(.no-cover) .card-headings .heading, preview-header:not(.no-co
margin-bottom: 0.6rem;
}
.header.preview-header {
align-items: start;
gap: 0.6rem;
min-height: unset;
padding-top: 0.6rem !important;
}
.header .headings {
width: unset;
flex-grow: 1;
padding-top: 0 !important;
display: flex;
flex-direction: column;
}
.header.has-cover {
min-height: calc(var(--header-height) / 2);
}
.header .title {
font-size: 40px;
}
.header .subtitle {
font-size: 32px;
}
.header-cover:not(:only-child) {
float: right;
height: var(--header-height);
max-width: calc(var(--header-height) * 2);
margin: 0 0 1.2rem 1.2rem;
}
.header-cover:only-child {
with: 100%;
}
@media screen and (max-width: 600px) {
.container.header {
width: 100%;
}
.container.header .headings {
width: 100%;
clear: both;
}
.container.header .header-cover {
float: none;
width: 100%;
max-width: unset;
height: unset;
margin-left: 0rem;
margin-right: 0rem;
}
}
.card-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
@ -430,6 +387,12 @@ preview-header:not(.no-cover) .card-headings .heading, preview-header:not(.no-co
.a-player a {
color: var(--a-player-url-fg);
}
.a-player .button {
color: var(--text-black);
}
.a-player .button:hover {
color: var(--button-fg);
}
.a-player-panels {
background: var(--a-player-panel-bg);
@ -2719,12 +2682,12 @@ a.navbar-item:focus, a.navbar-item:focus-within, a.navbar-item:hover, a.navbar-i
#player .button, #player a.button, #player button.button, #player .nav-urls a, .ax .button, .ax a.button, .ax button.button, .ax .nav-urls a {
display: inline-block;
padding: 0.6em;
border-radius: 4px;
padding: 0.4em;
border: 1px var(--highlight-color-2-alpha) solid;
justify-content: center;
text-align: center;
font-size: 1.4rem;
color: var(--button-fg);
background-color: var(--button-bg);
}
#player .button .icon, #player a.button .icon, #player button.button .icon, #player .nav-urls a .icon, .ax .button .icon, .ax a.button .icon, .ax button.button .icon, .ax .nav-urls a .icon {

View File

@ -33,9 +33,9 @@
--a-player-panel-bg: var(--highlight-color);
--a-player-bar-bg: var(--highlight-color);
--a-player-bar-title-alone-sz: 1.6rem;
--button-fg: var(--text-color);
--button-fg: var(--highlight-color-2);
--button-bg: var(--highlight-color);
--button-hg-fg: var(--highlight-color-2);
--button-hg-fg: var(--text-color);
--button-hg-bg: var(--highlight-color);
--button-active-fg: var(--highlight-color);
--button-active-bg: var(--highlight-color-2);
@ -234,6 +234,14 @@
height: var(--preview-cover-size);
width: var(--preview-cover-size);
}
.preview-card.small {
height: var(--preview-cover-small-size);
width: var(--preview-cover-small-size);
}
.preview-card.tiny {
height: var(--preview-cover-tiny-size);
width: var(--preview-cover-tiny-size);
}
.preview-card:not(.header) {
box-shadow: 0em 0em 1em rgba(0, 0, 0, 0.2);
}
@ -259,57 +267,6 @@ preview-header:not(.no-cover) .card-headings .heading, preview-header:not(.no-co
margin-bottom: 0.6rem;
}
.header.preview-header {
align-items: start;
gap: 0.6rem;
min-height: unset;
padding-top: 0.6rem !important;
}
.header .headings {
width: unset;
flex-grow: 1;
padding-top: 0 !important;
display: flex;
flex-direction: column;
}
.header.has-cover {
min-height: calc(var(--header-height) / 2);
}
.header .title {
font-size: 40px;
}
.header .subtitle {
font-size: 32px;
}
.header-cover:not(:only-child) {
float: right;
height: var(--header-height);
max-width: calc(var(--header-height) * 2);
margin: 0 0 1.2rem 1.2rem;
}
.header-cover:only-child {
with: 100%;
}
@media screen and (max-width: 600px) {
.container.header {
width: 100%;
}
.container.header .headings {
width: 100%;
clear: both;
}
.container.header .header-cover {
float: none;
width: 100%;
max-width: unset;
height: unset;
margin-left: 0rem;
margin-right: 0rem;
}
}
.card-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
@ -430,6 +387,12 @@ preview-header:not(.no-cover) .card-headings .heading, preview-header:not(.no-co
.a-player a {
color: var(--a-player-url-fg);
}
.a-player .button {
color: var(--text-black);
}
.a-player .button:hover {
color: var(--button-fg);
}
.a-player-panels {
background: var(--a-player-panel-bg);
@ -7381,6 +7344,9 @@ a.tag:hover {
text-decoration: none;
padding: 0.4rem;
}
.page a:hover {
color: var(--text-color);
}
.page section.container {
padding-top: 2rem;
}
@ -7419,12 +7385,12 @@ a.tag:hover {
.button, a.button, button.button, .nav-urls a {
display: inline-block;
padding: 0.6em;
border-radius: 4px;
padding: 0.4em;
border: 1px var(--highlight-color-2-alpha) solid;
justify-content: center;
text-align: center;
font-size: 1.4rem;
color: var(--button-fg);
background-color: var(--button-bg);
}
.button .icon, a.button .icon, button.button .icon, .nav-urls a .icon {
@ -7487,7 +7453,6 @@ a.tag:hover {
display: none;
}
.actions button, .actions .action {
background-color: var(--highlight-color);
justify-content: center;
min-width: 2rem;
}
@ -7500,9 +7465,6 @@ a.tag:hover {
.actions button label, .actions .action label {
margin-left: 0.4rem;
}
.actions button:hover, .actions button .selected, .actions .action:hover, .actions .action .selected {
color: var(--highlight-color-2) !important;
}
.label, .textarea, .input, .select {
font-size: 1.4rem;
@ -7520,6 +7482,10 @@ a.tag:hover {
margin-top: 0.6rem;
}
.navs {
position: relative;
}
.nav {
display: flex;
background-color: var(--highlight-color);
@ -7547,7 +7513,7 @@ a.tag:hover {
vertical-align: top;
display: inline-block;
}
.nav .nav-item.active {
.nav .nav-item.active, .nav .nav-item:hover {
background-color: var(--highlight-color-2);
color: var(--highlight-color);
}
@ -7581,12 +7547,32 @@ a.tag:hover {
white-space: nowrap;
}
.nav.secondary {
position: absolute;
width: 100%;
z-index: 100;
box-shadow: 0em 0.5em 0.5em rgba(0, 0, 0, 0.05);
justify-content: right;
display: none;
}
.nav-item:hover + .nav.secondary, .nav.secondary:hover {
display: flex;
top: var(--nav-primary-height);
left: 0rem;
}
.nav.secondary .nav-item {
font-size: 1rem;
}
.breadcrumbs {
text-align: right;
height: 2.2rem;
margin-bottom: 0.4rem;
}
.breadcrumbs a + a:before {
content: "/";
margin: 0 0.4rem;
}
@media screen and (max-width: 1024px) {
.page {
margin-top: var(--nav-primary-height);
@ -7685,7 +7671,7 @@ nav li a, nav li .button {
flex-direction: column;
}
.header.has-cover {
min-height: calc(var(--header-height) / 2);
min-height: calc(var(--header-height) / 3);
}
.header .title {
font-size: 40px;

View File

@ -375,7 +375,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac
\**********************/
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _fortawesome_fontawesome_free_css_all_min_css__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @fortawesome/fontawesome-free/css/all.min.css */ \"./node_modules/@fortawesome/fontawesome-free/css/all.min.css\");\n/* harmony import */ var _app__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./app */ \"./src/app.js\");\n/* harmony import */ var _vueLoader__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./vueLoader */ \"./src/vueLoader.js\");\n/* harmony import */ var _sound__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./sound */ \"./src/sound.js\");\n/* harmony import */ var _model__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./model */ \"./src/model.js\");\n/* harmony import */ var _assets_common_scss__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./assets/common.scss */ \"./src/assets/common.scss\");\n/**\n * This module includes code available for both the public website and\n * administration interface)\n */\n//-- vendor\n\n\n//-- aircox\n\n\n\n\n\nwindow.aircox = {\n // main application\n loader: null,\n get app() {\n return this.loader.app;\n },\n // player application\n playerLoader: null,\n get playerApp() {\n return this.playerLoader && this.playerLoader.app;\n },\n get player() {\n return this.playerLoader.vm && this.playerLoader.vm.$refs.player;\n },\n Set: _model__WEBPACK_IMPORTED_MODULE_4__.Set,\n Sound: _sound__WEBPACK_IMPORTED_MODULE_3__[\"default\"],\n /**\n * Initialize main application and player.\n */\n init(props = null, {\n hotReload = false,\n el = null,\n config = null,\n playerConfig = null,\n initApp = true,\n initPlayer = true,\n loader = null,\n playerLoader = null\n } = {}) {\n if (initPlayer) {\n playerConfig = playerConfig || _app__WEBPACK_IMPORTED_MODULE_1__.PlayerApp;\n playerLoader = playerLoader || new _vueLoader__WEBPACK_IMPORTED_MODULE_2__[\"default\"](playerConfig);\n playerLoader.enable(false);\n this.playerLoader = playerLoader;\n }\n if (initApp) {\n config = config || window.App || _app__WEBPACK_IMPORTED_MODULE_1__[\"default\"];\n config.el = el || config.el;\n loader = loader || new _vueLoader__WEBPACK_IMPORTED_MODULE_2__[\"default\"]({\n el,\n props,\n ...config\n });\n loader.enable(hotReload);\n this.loader = loader;\n }\n },\n /**\n * Filter navbar dropdown menu items\n */\n filter_menu(event) {\n var filter = new RegExp(event.target.value, 'gi');\n var container = event.target.closest('.navbar-dropdown');\n if (event.target.value) for (let item of container.querySelectorAll('a.navbar-item')) item.style.display = item.innerHTML.search(filter) == -1 ? 'none' : null;else for (let item of container.querySelectorAll('a.navbar-item')) item.style.display = null;\n },\n pickDate(url, date) {\n url = `${url}?date=${date.id}`;\n this.builder.fetch(url);\n }\n};\n\n//# sourceURL=webpack://aircox-assets/./src/index.js?");
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _fortawesome_fontawesome_free_css_all_min_css__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @fortawesome/fontawesome-free/css/all.min.css */ \"./node_modules/@fortawesome/fontawesome-free/css/all.min.css\");\n/* harmony import */ var _app__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./app */ \"./src/app.js\");\n/* harmony import */ var _vueLoader__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./vueLoader */ \"./src/vueLoader.js\");\n/* harmony import */ var _sound__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./sound */ \"./src/sound.js\");\n/* harmony import */ var _model__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./model */ \"./src/model.js\");\n/* harmony import */ var _assets_common_scss__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./assets/common.scss */ \"./src/assets/common.scss\");\n/**\n * This module includes code available for both the public website and\n * administration interface)\n */\n//-- vendor\n\n\n//-- aircox\n\n\n\n\n\nwindow.aircox = {\n // main application\n loader: null,\n get app() {\n return this.loader.app;\n },\n // player application\n playerLoader: null,\n get playerApp() {\n return this.playerLoader && this.playerLoader.app;\n },\n get player() {\n return this.playerLoader.vm && this.playerLoader.vm.$refs.player;\n },\n Set: _model__WEBPACK_IMPORTED_MODULE_4__.Set,\n Sound: _sound__WEBPACK_IMPORTED_MODULE_3__[\"default\"],\n /**\n * Initialize main application and player.\n */\n init(props = null, {\n hotReload = false,\n el = null,\n config = null,\n playerConfig = null,\n initApp = true,\n initPlayer = true,\n loader = null,\n playerLoader = null\n } = {}) {\n if (initPlayer) {\n playerConfig = playerConfig || _app__WEBPACK_IMPORTED_MODULE_1__.PlayerApp;\n playerLoader = playerLoader || new _vueLoader__WEBPACK_IMPORTED_MODULE_2__[\"default\"](playerConfig);\n playerLoader.enable(false);\n this.playerLoader = playerLoader;\n }\n if (initApp) {\n config = config || window.App || _app__WEBPACK_IMPORTED_MODULE_1__[\"default\"];\n config.el = el || config.el;\n loader = loader || new _vueLoader__WEBPACK_IMPORTED_MODULE_2__[\"default\"]({\n el,\n props,\n ...config\n });\n loader.enable(hotReload);\n this.loader = loader;\n }\n },\n /**\n * Filter navbar dropdown menu items\n */\n filter_menu(event) {\n var filter = new RegExp(event.target.value, 'gi');\n var container = event.target.closest('.navbar-dropdown');\n if (event.target.value) for (let item of container.querySelectorAll('a.navbar-item')) item.style.display = item.innerHTML.search(filter) == -1 ? 'none' : null;else for (let item of container.querySelectorAll('a.navbar-item')) item.style.display = null;\n },\n pickDate(url, date) {\n url = `${url}?date=${date.id}`;\n this.loader.pageLoad.load(url);\n }\n};\n\n//# sourceURL=webpack://aircox-assets/./src/index.js?");
/***/ }),

View File

@ -14,7 +14,7 @@
{% if diffusions %}
<div class="card-grid">
{% for obj in diffusions %}
{% page_widget "card" obj.episode diffusion=obj timetable=True admin=True tag_class="small" %}
{% page_widget "card" obj.episode diffusion=obj timetable=True admin=True tag_class="" %}
{% endfor %}
</div>
{% else %}

View File

@ -61,6 +61,22 @@ Usefull context:
aria-label="{% translate "Main menu" %}">
</a-switch>
<div class="nav-menu">
{% for nav_item, secondary in nav_menu %}
<a class="nav-item" href="{{ nav_item.get_url }}">
{{ nav_item.get_label }}
</a>
{% if secondary %}
<div class="nav secondary">
{% for label, url in secondary %}
<a href="{{ url }}" class="nav-item">
{{ label }}
</a>
{% endfor %}
</div>
{% endif %}
{% endfor %}
{% comment %}
{% block nav-primary-menu %}
{% nav_items "top" css_class="nav-item" active_class="active" as items %}
{% for item, render in items %}
@ -72,6 +88,7 @@ Usefull context:
</a>
{% endif %}
{% endblock %}
{% endcomment %}
</div>
{% endblock %}
</nav>
@ -79,6 +96,14 @@ Usefull context:
{% block secondary-nav %}{% endblock %}
</div>
{% spaceless %}
{% block breadcrumbs-container %}
<div class="breadcrumbs container">
{% block breadcrumbs %}{% endblock %}
</div>
{% endblock %}
{% endspaceless %}
{% block main-container %}
<main class="page">
{% block main %}
@ -99,20 +124,6 @@ Usefull context:
{% block subtitle %}
{% if subtitle %}
{{ subtitle }}
{% elif parent and parent.is_published %}
<a href="{{ parent.get_absolute_url|escape }}" class="heading subtitle">
<span class="icon">
<i class="fa fa-angles-right"></i>
</span>
{{ parent.title }}
</a>
{% elif page and page.category %}
<a href="{% url "page-list" %}?category__id={{ page.category.id }}">
<span class="icon">
<i class="fa fa-angles-right"></i>
</span>
{{ page.category.title }}
</a>
{% endif %}
{% endblock %}
</span>

View File

@ -3,6 +3,8 @@
{% block head_title %}{{ station.name }}{% endblock %}
{% block breadcrumbs-container %}{% endblock %}
{% block content-container %}
{{ block.super }}
@ -40,7 +42,7 @@
{% endfor %}
<nav class="nav-urls">
<a href="{% url "log-list" %}"
<a href="{% url "timetable-list" %}"
aria-label="{% translate "Show all program's for today" %}">
{% translate "Today" %}
</a>

View File

@ -23,6 +23,22 @@ Context:
{% endif %}
{% endblock %}
{% block breadcrumbs %}
{% if parent %}
{% include "./widgets/breadcrumbs.html" with page=parent %}
{% if page %}
<a href="{% url page.list_url_name parent_slug=parent.slug %}">
{{ page|verbose_name:True }}
</a>
{% endif %}
{% elif page %}
{% include "./widgets/breadcrumbs.html" with page=page no_title=True %}
{% endif %}
{% endblock %}
{% block main %}
{{ block.super }}
@ -31,21 +47,9 @@ Context:
<section class="container">
{% with models=object|verbose_name:True %}
<h3 class="title is-3">{% blocktranslate %}Related {{models}}{% endblocktranslate %}</h3>
{% include "./widgets/carousel.html" with objects=related_objects url_name=object.list_url_name url_category=object.category %}
{% endwith %}
<a-carousel section-class="card-grid">
{% for object in related_objects %}
{% page_widget "card" object %}
{% endfor %}
</a-carousel>
{% if related_url %}
<nav class="nav-urls">
<a href="{{ related_url }}">
{% translate "See more" %}
</a>
</nav>
{% endif %}
{% endif %}
{% endblock %}

View File

@ -3,12 +3,14 @@
{% load i18n aircox %}
{% block secondary-nav %}
{% if view.categories %}
{% if categories %}
<nav class="nav secondary">
<div class="nav-menu nav-categories">
{% for id, title in view.categories.items %}
<a class="nav-item{% if category_id == id or parent and parent.category_id == id %} active{% endif %}"
href="?category__id={{ id }}">{{ title }}</a>
{% for cat in categories %}
<a class="nav-item{% if cat == category %} active{% endif %}"
href="{% url request.resolver_match.url_name category_slug=cat.slug %}">
{{ cat.title }}
</a>
{% endfor %}
</div>
<a-switch class="button burger"
@ -19,6 +21,18 @@
{% endif %}
{% endblock %}
{% block title %}
{% if parent %}{{ parent.title }}
{% else %}{{ block.super }}
{% endif %}
{% endblock %}
{% block content %}
{% if parent %}{{ parent.content|safe }}
{% else %}{{ block.super }}
{% endif %}
{% endblock %}
{% block header %}
{% if page and not object %}
{% with page as object %}
@ -28,3 +42,19 @@
{{ block.super }}
{% endif %}
{% endblock %}
{% block breadcrumbs %}
{% if parent %}
{% include "./widgets/breadcrumbs.html" with page=parent %}
<a href="{{ request.path }}">{{ model|verbose_name:True }}</a>
{% elif page %}
<a href="{{ request.path }}">{{ page.title }}</a>
{% else %}
<a href="{% url request.resolver_match.url_name %}">{{ model|verbose_name:True }}</a>
{% if category %}
<a href="{% url request.resolver_match.url_name category_slug=category.slug %}">
{{ category.title }}
</a>
{% endif %}
{% endif %}
{% endblock %}

View File

@ -0,0 +1,31 @@
{% extends "aircox/page_list.html" %}
{% comment %}List of diffusions as a timetable{% endcomment %}
{% load i18n aircox humanize %}
{% block subtitle %}{{ date|date:"l d F Y" }}{% endblock %}
{% block secondary-nav %}
<nav class="nav secondary">
{% include "./widgets/dates_menu.html" with url_name=view.redirect_date_url %}
</nav>
{% endblock %}
{% block breadcrumbs %}
{{ block.super }}
<a href="{% url "timetable-list" date=date %}">{{ date|date:"l d F Y" }}</a>
{% endblock %}
{% block pages_list %}
{% with hide_schedule=True %}
<section role="list" class="list">
{% for object in object_list %}
{% if object.episode %}
{% page_widget "item" object.episode diffusion=object timetable=True %}
{% else %}
{% page_widget "item" object timetable=True %}
{% endif %}
{% endfor %}
</section>
{% endwith %}
{% endblock %}

View File

@ -0,0 +1,15 @@
{% load aircox %}
<a href="{% url page.list_url_name %}">
{{ page|verbose_name:True }}
</a>
{% if page.category and not no_cat %}
<a href="{% url page.list_url_name category_slug=page.category.slug %}">
{{ page.category.title }}
</a>
{% endif %}
{% if not no_title %}
<a href="{{ page.get_absolute_url }}">
{{ page.title|truncatechars:24 }}
</a>
{% endif %}

View File

@ -16,7 +16,9 @@ Context:
{% if url_name %}
<nav class="nav-urls">
{% if url_parent %}
<a href="{% url url_name parent_slug=program.slug %}">
<a href="{% url url_name parent_slug=url_parent.slug %}">
{% elif url_category %}
<a href="{% url url_name category_slug=url_category.slug %}">
{% else %}
<a href="{% url url_name %}">
{% endif %}

View File

@ -40,7 +40,8 @@ The audio player
<h4 v-else-if="live && current && current.data.type == 'diffusion'"
class="title"
aria-description="{% translate "Diffusion currently on air" %}">
<a :href="current.data.url">[[ current.data.title ]]</a>
<a :href="current.data.url" v-if="current.data.url">[[ current.data.title ]]</a>
<template v-else>[[ current.data.title ]]</template>
</h4>
<h4 v-else class="title is-4" aria-description="{% translate "Currently playing" %}">
{{ request.station.name }}

View File

@ -38,38 +38,47 @@ api = [
urls = [
path("", views.HomeView.as_view(), name="home"),
path("api/", include((api, "aircox"), namespace="api")),
# path('', views.PageDetailView.as_view(model=models.Article),
# name='home'),
# ---- ---- objects views
# ---- articles
path(
_("articles/"),
views.ArticleListView.as_view(model=models.Article),
name="article-list",
),
path(
_("articles/c/<slug:category_slug>/"), views.ArticleListView.as_view(model=models.Article), name="article-list"
),
path(
_("articles/<slug:slug>/"),
views.ArticleDetailView.as_view(),
name="article-detail",
),
# ---- episodes
path(_("episodes/"), views.EpisodeListView.as_view(), name="episode-list"),
path(_("episodes/c/<slug:category_slug>"), views.EpisodeListView.as_view(), name="episode-list"),
path(
_("episodes/<slug:slug>/"),
views.EpisodeDetailView.as_view(),
name="episode-detail",
),
path(_("podcasts/"), views.PodcastListView.as_view(), name="podcast-list"),
path(_("podcasts/<slug:parent_slug>"), views.PodcastListView.as_view(), name="podcast-list"),
path(_("week/"), views.DiffusionListView.as_view(), name="diffusion-list"),
path(_("podcasts/c/<slug:category_slug>/"), views.PodcastListView.as_view(), name="podcast-list"),
# ---- timetable
path(_("timetable/"), views.TimeTableView.as_view(), name="timetable-list"),
path(
_("week/<date:date>/"),
views.DiffusionListView.as_view(),
name="diffusion-list",
_("timetable/<date:date>/"),
views.TimeTableView.as_view(),
name="timetable-list",
),
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'),
# ---- pages
path(
_("publications/"),
views.PageListView.as_view(model=models.Page, attach_to_value=models.StaticPage.ATTACH_TO_PAGES),
views.PageListView.as_view(model=models.Page, attach_to_value=models.StaticPage.Target.PAGES),
name="page-list",
),
path(
_("publications/c/<slug:category_slug>"),
views.PageListView.as_view(model=models.Page, attach_to_value=models.StaticPage.Target.PAGES),
name="page-list",
),
path(
@ -88,27 +97,18 @@ urls = [
),
name="static-page-detail",
),
# ---- programs
path(_("programs/"), views.ProgramListView.as_view(), name="program-list"),
path(_("programs/c/<slug:category_slug>"), views.ProgramListView.as_view(), 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="program-page-list",
),
path(_("program/<slug:parent_slug>/articles"), views.ArticleListView.as_view(), name="article-list"),
path(_("program/<slug:parent_slug>/podcasts"), views.PodcastListView.as_view(), name="podcast-list"),
path(_("program/<slug:parent_slug>/episodes"), views.EpisodeListView.as_view(), name="episode-list"),
path(_("program/<slug:parent_slug>/diffusions"), views.DiffusionListView.as_view(), name="diffusion-list"),
path(
"errors/no-station",
views.errors.NoStationErrorView.as_view(),

View File

@ -1,7 +1,7 @@
from . import admin, errors
from .article import ArticleDetailView, ArticleListView
from .base import BaseAPIView, BaseView
from .diffusion import DiffusionListView
from .diffusion import DiffusionListView, TimeTableView
from .episode import EpisodeDetailView, EpisodeListView, PodcastListView
from .home import HomeView
from .log import LogListAPIView, LogListView
@ -26,6 +26,7 @@ __all__ = (
"BaseAPIView",
"BaseView",
"DiffusionListView",
"TimeTableView",
"EpisodeDetailView",
"EpisodeListView",
"PodcastListView",
@ -40,4 +41,15 @@ __all__ = (
"ProgramListView",
"ProgramPageDetailView",
"ProgramPageListView",
"attached",
)
attached = {}
for key in __all__:
view = globals().get(key)
if key == "attached":
continue
if attach := getattr(view, "attach_to_value", None):
attached[attach] = view

View File

@ -16,4 +16,4 @@ class ArticleListView(PageListView):
model = Article
has_headline = True
parent_model = Program
attach_to_value = StaticPage.ATTACH_TO_ARTICLES
attach_to_value = StaticPage.Target.ARTICLES

View File

@ -18,6 +18,26 @@ class BaseView(TemplateResponseMixin, ContextMixin):
# def get_queryset(self):
# return super().get_queryset().station(self.station)
def get_nav_menu(self):
menu = []
for item in self.station.navitem_set.all():
try:
if item.page:
view = item.page.get_related_view()
secondary = view and view.get_secondary_nav()
else:
secondary = None
menu.append((item, secondary))
except:
import traceback
traceback.print_exc()
raise
return menu
def get_secondary_nav(self):
return None
def get_related_queryset(self):
"""Return a queryset of related pages or None."""
return None
@ -46,6 +66,8 @@ class BaseView(TemplateResponseMixin, ContextMixin):
kwargs["title"] = page.display_title
kwargs["cover"] = page.cover and page.cover.url
if "nav_menu" not in kwargs:
kwargs["nav_menu"] = self.get_nav_menu()
return super().get_context_data(**kwargs)
def dispatch(self, *args, **kwargs):

View File

@ -1,29 +1,54 @@
import datetime
from django.urls import reverse
from django.views.generic import ListView
from aircox.models import Diffusion, StaticPage
from aircox.models import Diffusion, Log, StaticPage
from .base import BaseView
from .mixins import AttachedToMixin, GetDateMixin
__all__ = ("DiffusionListView",)
__all__ = ("DiffusionListView", "TimeTableView")
class DiffusionListView(GetDateMixin, AttachedToMixin, BaseView, ListView):
class BaseDiffusionListView(AttachedToMixin, BaseView, ListView):
model = Diffusion
queryset = Diffusion.objects.on_air().order_by("-start")
class DiffusionListView(BaseDiffusionListView):
"""View for timetables."""
model = Diffusion
redirect_date_url = "diffusion-list"
attach_to_value = StaticPage.ATTACH_TO_DIFFUSIONS
class TimeTableView(GetDateMixin, BaseDiffusionListView):
model = Diffusion
redirect_date_url = "timetable-list"
attach_to_value = StaticPage.Target.TIMETABLE
template_name = "aircox/timetable_list.html"
def get_date(self):
date = super().get_date()
return date if date is not None else datetime.date.today()
def get_queryset(self):
return super().get_queryset().date(self.date).order_by("start")
def get_logs(self, date):
return Log.objects.on_air().date(self.date).filter(track__isnull=False)
def get_context_data(self, **kwargs):
def get_queryset(self):
return super().get_queryset().date(self.date)
@classmethod
def get_secondary_nav(cls):
date = datetime.date.today()
start = date - datetime.timedelta(days=date.weekday())
dates = [start + datetime.timedelta(days=i) for i in range(0, 7)]
return tuple((date.strftime("%A %d"), reverse("timetable-list", kwargs={"date": date})) for date in dates)
def get_context_data(self, object_list=None, **kwargs):
start = self.date - datetime.timedelta(days=self.date.weekday())
dates = [start + datetime.timedelta(days=i) for i in range(0, 7)]
return super().get_context_data(date=self.date, dates=dates, **kwargs)
if object_list is None:
logs = self.get_logs(self.date)
object_list = Log.merge_diffusions(logs, self.object_list)
return super().get_context_data(date=self.date, dates=dates, object_list=object_list, **kwargs)

View File

@ -33,9 +33,9 @@ class EpisodeListView(PageListView):
model = Episode
filterset_class = EpisodeFilters
parent_model = Program
attach_to_value = StaticPage.ATTACH_TO_EPISODES
attach_to_value = StaticPage.Target.EPISODES
class PodcastListView(EpisodeListView):
attach_to_value = StaticPage.ATTACH_TO_PODCASTS
attach_to_value = StaticPage.Target.PODCASTS
queryset = Episode.objects.published().with_podcasts().order_by("-pub_date")

View File

@ -10,7 +10,7 @@ from .mixins import AttachedToMixin
class HomeView(AttachedToMixin, BaseView, ListView):
template_name = "aircox/home.html"
attach_to_value = StaticPage.ATTACH_TO_HOME
attach_to_value = StaticPage.Target.HOME
model = Diffusion
queryset = Diffusion.objects.on_air().select_related("episode").order_by("-start")

View File

@ -6,12 +6,12 @@ from django.views.decorators.cache import cache_page
from django.views.generic import ListView
from rest_framework.generics import ListAPIView
from ..models import Diffusion, Log, StaticPage
from ..models import Diffusion, Log
from ..serializers import LogInfo, LogInfoSerializer
from .base import BaseAPIView, BaseView
from .mixins import AttachedToMixin, GetDateMixin
__all__ = ["LogListMixin", "LogListView"]
__all__ = ("LogListMixin", "LogListView", "LogListAPIView")
class LogListMixin(GetDateMixin):
@ -62,7 +62,6 @@ class LogListView(AttachedToMixin, BaseView, LogListMixin, ListView):
`request.GET`, defaults to today)."""
redirect_date_url = "log-list"
attach_to_value = StaticPage.ATTACH_TO_LOGS
def get_date(self):
date = super().get_date()

View File

@ -44,16 +44,16 @@ class ParentMixin:
parent = None
"""Parent page object."""
def get_parent(self, request, *args, **kwargs):
def get_parent(self, request, **kwargs):
if self.parent_model is None or self.parent_url_kwarg not in kwargs:
return
lookup = {self.parent_field: kwargs[self.parent_url_kwarg]}
return get_object_or_404(self.parent_model.objects.select_related("cover"), **lookup)
def get(self, request, *args, **kwargs):
self.parent = self.get_parent(request, *args, **kwargs)
return super().get(request, *args, **kwargs)
def dispatch(self, request, *args, **kwargs):
self.parent = self.get_parent(request, **kwargs)
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
if self.parent is not None:
@ -61,9 +61,10 @@ class ParentMixin:
return super().get_queryset()
def get_context_data(self, **kwargs):
self.parent = kwargs.setdefault("parent", self.parent)
if self.parent is not None:
kwargs.setdefault("cover", self.parent.cover.url)
parent = kwargs.setdefault("parent", self.parent)
if parent is not None:
kwargs.setdefault("cover", parent.cover.url)
return super().get_context_data(**kwargs)

View File

@ -1,11 +1,12 @@
from django.http import Http404, HttpResponse
from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, ListView
from django.urls import reverse
from honeypot.decorators import check_honeypot
from ..filters import PageFilters
from ..forms import CommentForm
from ..models import Comment
from ..models import Comment, Category
from ..utils import Redirect
from .base import BaseView
from .mixins import AttachedToMixin, FiltersMixin, ParentMixin
@ -18,7 +19,25 @@ __all__ = [
]
class BasePageListView(AttachedToMixin, ParentMixin, BaseView, ListView):
class BasePageMixin:
category = None
def get_category(self, page, **kwargs):
if page:
if getattr(page, "category_id", None):
return page.category
if page.parent_id:
return self.get_category(page.parent_subclass)
if slug := self.kwargs.get("category_slug"):
return Category.objects.get(slug=slug)
return None
def get_context_data(self, *args, **kwargs):
kwargs.setdefault("category", self.category)
return super().get_context_data(*args, **kwargs)
class BasePageListView(AttachedToMixin, BasePageMixin, ParentMixin, BaseView, ListView):
"""Base view class for BasePage list."""
template_name = "aircox/basepage_list.html"
@ -26,11 +45,18 @@ class BasePageListView(AttachedToMixin, ParentMixin, BaseView, ListView):
paginate_by = 30
has_headline = True
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def get(self, *args, **kwargs):
self.category = self.get_category(self.parent)
return super().get(*args, **kwargs)
def get_queryset(self):
return super().get_queryset().select_subclasses().published().select_related("cover")
query = super().get_queryset().select_subclasses().published().select_related("cover")
if self.category:
query = query.filter(category=self.category)
return query
def get_context_data(self, **kwargs):
kwargs.setdefault("has_headline", self.has_headline)
@ -48,12 +74,15 @@ class BasePageListView(AttachedToMixin, ParentMixin, BaseView, ListView):
return context
class BasePageDetailView(BaseView, DetailView):
class BasePageDetailView(BasePageMixin, BaseView, DetailView):
"""Base view class for BasePage."""
template_name = "aircox/basepage_detail.html"
context_object_name = "page"
def get(self, *args, **kwargs):
return super().get(*args, **kwargs)
def get_context_data(self, **kwargs):
if self.object.cover:
kwargs.setdefault("cover", self.object.cover.url)
@ -81,6 +110,8 @@ class BasePageDetailView(BaseView, DetailView):
if redirect_url:
raise Redirect(redirect_url)
raise Http404("%s not found" % self.model._meta.verbose_name)
self.category = self.get_category(obj)
return obj
def get_page(self):
@ -105,26 +136,22 @@ class PageListView(FiltersMixin, BasePageListView):
def get_queryset(self):
qs = super().get_queryset().select_related("category").order_by("-pub_date")
cat_ids = self.model.objects.published().values_list("category_id", flat=True)
self.categories = Category.objects.filter(id__in=cat_ids)
return qs
@classmethod
def get_secondary_nav(cls):
cat_ids = cls.model.objects.published().values_list("category_id", flat=True)
categories = Category.objects.filter(id__in=cat_ids)
return tuple(
(category.title, reverse(cls.model.list_url_name, kwargs={"category_slug": category.slug}))
for category in categories
)
def get_context_data(self, **kwargs):
self.categories = {
id: title
for title, id in self.model.objects.published()
.filter(category__isnull=False)
.values_list("category__title", "category__id")
.distinct()
}
cat_id = self.request.GET.get("category__id")
if cat_id:
cat_id = int(cat_id)
kwargs["category_id"] = cat_id
context = super().get_context_data(**kwargs)
if context.get("parent") and not cat_id:
kwargs["category_id"] = context["parent"].category_id
return context
kwargs["categories"] = self.categories
return super().get_context_data(**kwargs)
class PageDetailView(BasePageDetailView):
@ -152,7 +179,6 @@ class PageDetailView(BasePageDetailView):
if related:
related = related[: self.related_count]
kwargs["related_objects"] = related
kwargs["related_url"] = self.get_related_url()
return super().get_context_data(**kwargs)
@classmethod

View File

@ -52,7 +52,7 @@ class ProgramDetailView(BaseProgramMixin, PageDetailView):
class ProgramListView(PageListView):
model = Program
attach_to_value = StaticPage.ATTACH_TO_PROGRAMS
attach_to_value = StaticPage.Target.PROGRAMS
# FIXME: not used

View File

@ -39,9 +39,9 @@
--a-player-bar-bg: var(--highlight-color);
--a-player-bar-title-alone-sz: #{v.$text-size-bigger};
--button-fg: var(--text-color);
--button-fg: var(--highlight-color-2);
--button-bg: var(--highlight-color);
--button-hg-fg: var(--highlight-color-2);
--button-hg-fg: var(--text-color);
--button-hg-bg: var(--highlight-color);
--button-active-fg: var(--highlight-color);
--button-active-bg: var(--highlight-color-2);
@ -102,13 +102,13 @@
@mixin button {
.button, a.button, button.button, .nav-urls a {
display: inline-block;
padding: v.$mp-3e;
border-radius: 4px;
padding: v.$mp-2e;
border: 1px var(--highlight-color-2-alpha) solid;
justify-content: center;
text-align: center;
font-size: v.$text-size-medium;
color: var(--button-fg);
background-color: var(--button-bg);
.icon {
@ -169,7 +169,6 @@
}
// ---- preview
.preview {
position: relative;
@ -355,6 +354,15 @@
height: var(--preview-cover-size);
width: var(--preview-cover-size);
&.small {
height: var(--preview-cover-small-size);
width: var(--preview-cover-small-size);
}
&.tiny {
height: var(--preview-cover-tiny-size);
width: var(--preview-cover-tiny-size);
}
&:not(.header) {
box-shadow: 0em 0em 1em rgba(0,0,0,0.2);
}
@ -389,72 +397,6 @@
}
// ---- page header
.header {
&.preview-header {
//display: flex;
align-items: start;
gap: v.$mp-3;
min-height: unset;
padding-top: v.$mp-3 !important;
}
.headings {
width: unset;
flex-grow: 1;
padding-top: 0 !important;
display: flex;
flex-direction: column;
}
&.has-cover {
min-height: calc( var(--header-height) / 2 );
}
.title {
font-size: v.$h1-size;
}
.subtitle {
font-size: v.$h2-size;
}
}
.header-cover:not(:only-child) {
float: right;
height: var(--header-height);
max-width: calc(var(--header-height) * 2);
margin: 0 0 v.$mp-4 v.$mp-4;
}
.header-cover:only-child {
with: 100%;
}
@media screen and (max-width: v.$screen-small) {
.container.header {
width: 100%;
.headings {
width: 100%;
clear: both;
}
.header-cover {
float: none;
width: 100%;
max-width: unset;
height: unset;
margin-left: 0rem;
margin-right: 0rem;
}
}
}
// ---- card grid
.card-grid {
@ -606,6 +548,10 @@
box-shadow: 0em -0.5em 0.5em rgba(0, 0, 0, 0.05);
a { color: var(--a-player-url-fg); }
.button {
color: var(--text-black);
&:hover { color: var(--button-fg); }
}
}
.a-player-panels {

View File

@ -13,8 +13,13 @@
color: var(--highlight-color-2);
text-decoration: none;
padding: v.$mp-2;
&:hover {
color: var(--text-color);
}
}
section.container {
padding-top: v.$mp-6;
@ -74,22 +79,12 @@
}
button, .action {
background-color: var(--highlight-color);
justify-content: center;
min-width: 2rem;
.not-selected { opacity: 0.6; }
.icon { margin: 0em !important; }
label {
margin-left: v.$mp-2;
}
&:hover, .selected {
color: var(--highlight-color-2) !important;
}
label { margin-left: v.$mp-2; }
}
}
@ -115,6 +110,10 @@
}
// ---- main navigation
.navs {
position: relative;
}
.nav {
display: flex;
background-color: var(--highlight-color);
@ -145,7 +144,7 @@
display: inline-block;
}
&.active {
&.active, &:hover {
background-color: var(--highlight-color-2);
color: var(--highlight-color);
}
@ -189,7 +188,19 @@
}
&.secondary {
position: absolute;
width: 100%;
z-index: 100;
box-shadow: 0em 0.5em 0.5em rgba(0, 0, 0, 0.05);
justify-content: right;
display: none;
.nav-item:hover + &, &:hover {
display: flex;
top: var(--nav-primary-height);
left: 0rem;
}
.nav-item {
font-size: v.$text-size;
@ -197,6 +208,18 @@
}
}
// ---- breadcrumbs
.breadcrumbs {
text-align: right;
height: calc( v.$mp-3 * 2 + v.$text-size);
margin-bottom: v.$mp-2;
a + a:before {
content: "/";
margin: 0 v.$mp-2;
}
}
@media screen and (max-width: v.$screen-normal) {
.page {
@ -324,7 +347,7 @@ nav li {
}
&.has-cover {
min-height: calc( var(--header-height) / 2 );
min-height: calc( var(--header-height) / 3 );
}
.title {

View File

@ -69,6 +69,6 @@ window.aircox = {
pickDate(url, date) {
url = `${url}?date=${date.id}`
this.builder.fetch(url)
this.loader.pageLoad.load(url)
}
}