diff --git a/aircox/admin/article.py b/aircox/admin/article.py index b8248d2..4a08dc9 100644 --- a/aircox/admin/article.py +++ b/aircox/admin/article.py @@ -11,11 +11,8 @@ __all__ = ['ArticleAdmin'] @admin.register(Article) class ArticleAdmin(PageAdmin): - list_display = PageAdmin.list_display + ('program',) - list_filter = PageAdmin.list_filter + ('program',) - search_fields = PageAdmin.search_fields + ['program__title'] + list_filter = PageAdmin.list_filter + search_fields = PageAdmin.search_fields + ['parent__title'] # TODO: readonly field - fieldsets = copy.deepcopy(PageAdmin.fieldsets) - fieldsets[1][1]['fields'].insert(0, 'program') diff --git a/aircox/admin/episode.py b/aircox/admin/episode.py index b41ce2f..9e91d44 100644 --- a/aircox/admin/episode.py +++ b/aircox/admin/episode.py @@ -48,13 +48,11 @@ class DiffusionInline(DiffusionBaseAdmin, admin.TabularInline): @admin.register(Episode) class EpisodeAdmin(PageAdmin): - list_display = PageAdmin.list_display + ('program',) - list_filter = PageAdmin.list_filter + ('program',) - search_fields = PageAdmin.search_fields + ['program__title'] - readonly_fields = ('program',) + list_display = PageAdmin.list_display + list_filter = PageAdmin.list_filter + search_fields = PageAdmin.search_fields + ['parent__title'] + # readonly_fields = ('parent',) - fieldsets = copy.deepcopy(PageAdmin.fieldsets) - fieldsets[1][1]['fields'].insert(0, 'program') inlines = [TracksInline, SoundInline, DiffusionInline] diff --git a/aircox/admin/page.py b/aircox/admin/page.py index 6cb5bc1..a139f5c 100644 --- a/aircox/admin/page.py +++ b/aircox/admin/page.py @@ -21,7 +21,7 @@ class CategoryAdmin(admin.ModelAdmin): # limit category choice class PageAdmin(admin.ModelAdmin): - list_display = ('cover_thumb', 'title', 'status', 'category') + list_display = ('cover_thumb', 'title', 'status', 'category', 'parent') list_display_links = ('cover_thumb', 'title') list_editable = ('status', 'category') list_filter = ('status', 'category') @@ -33,7 +33,7 @@ class PageAdmin(admin.ModelAdmin): 'fields': ['title', 'slug', 'category', 'cover', 'content'], }), (_('Publication Settings'), { - 'fields': ['featured', 'allow_comments', 'status'], + 'fields': ['featured', 'allow_comments', 'status', 'parent'], 'classes': ('collapse',), }), ] diff --git a/aircox/converters.py b/aircox/converters.py index 6b3d422..f4030dd 100644 --- a/aircox/converters.py +++ b/aircox/converters.py @@ -33,7 +33,8 @@ class WeekConverter: return datetime.datetime.strptime(value + '/1', '%G/%V/%u').date() def to_url(self, value): - return '{:04d}/{:02d}'.format(*value.isocalendar()) + return value if isinstance(value, str) else \ + '{:04d}/{:02d}'.format(*value.isocalendar()) class DateConverter: @@ -41,8 +42,9 @@ class DateConverter: regex = r'[0-9]{4}/[0-9]{2}/[0-9]{2}' def to_python(self, value): - return str_to_date(value) + value = value.split('/')[:3] + return datetime.date(int(value[0]), int(value[1]), int(value[2])) def to_url(self, value): - return '{:04d}/{:02d}/{:02d}'.format(value.year, value.month, - value.day) + return value if isinstance(value, str) else \ + '{:04d}/{:02d}/{:02d}'.format(value.year, value.month, value.day) diff --git a/aircox/models/__pycache__/__init__.cpython-37.pyc b/aircox/models/__pycache__/__init__.cpython-37.pyc index f6e2bd5..cfebf45 100644 Binary files a/aircox/models/__pycache__/__init__.cpython-37.pyc and b/aircox/models/__pycache__/__init__.cpython-37.pyc differ diff --git a/aircox/models/__pycache__/episode.cpython-37.pyc b/aircox/models/__pycache__/episode.cpython-37.pyc index 86897a7..6059e25 100644 Binary files a/aircox/models/__pycache__/episode.cpython-37.pyc and b/aircox/models/__pycache__/episode.cpython-37.pyc differ diff --git a/aircox/models/__pycache__/log.cpython-37.pyc b/aircox/models/__pycache__/log.cpython-37.pyc index 55a125e..0271250 100644 Binary files a/aircox/models/__pycache__/log.cpython-37.pyc and b/aircox/models/__pycache__/log.cpython-37.pyc differ diff --git a/aircox/models/__pycache__/page.cpython-37.pyc b/aircox/models/__pycache__/page.cpython-37.pyc index a02bef3..1e1b877 100644 Binary files a/aircox/models/__pycache__/page.cpython-37.pyc and b/aircox/models/__pycache__/page.cpython-37.pyc differ diff --git a/aircox/models/__pycache__/program.cpython-37.pyc b/aircox/models/__pycache__/program.cpython-37.pyc index a9b7a10..07c00dd 100644 Binary files a/aircox/models/__pycache__/program.cpython-37.pyc and b/aircox/models/__pycache__/program.cpython-37.pyc differ diff --git a/aircox/models/__pycache__/sound.cpython-37.pyc b/aircox/models/__pycache__/sound.cpython-37.pyc index 0675e48..db47a40 100644 Binary files a/aircox/models/__pycache__/sound.cpython-37.pyc and b/aircox/models/__pycache__/sound.cpython-37.pyc differ diff --git a/aircox/models/__pycache__/station.cpython-37.pyc b/aircox/models/__pycache__/station.cpython-37.pyc index 70663bb..fbb7e58 100644 Binary files a/aircox/models/__pycache__/station.cpython-37.pyc and b/aircox/models/__pycache__/station.cpython-37.pyc differ diff --git a/aircox/models/article.py b/aircox/models/article.py index c631632..259bf2e 100644 --- a/aircox/models/article.py +++ b/aircox/models/article.py @@ -2,28 +2,20 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from .page import Page, PageQuerySet -from .program import Program, InProgramQuerySet - - -class ArticleQuerySet(InProgramQuerySet, PageQuerySet): - pass +from .program import Program, ProgramChildQuerySet class Article(Page): detail_url_name = 'article-detail' - program = models.ForeignKey( - Program, models.SET_NULL, - verbose_name=_('program'), blank=True, null=True, - help_text=_("publish as this program's article"), - ) is_static = models.BooleanField( _('is static'), default=False, help_text=_('Should this article be considered as a page ' 'instead of a blog article'), ) - objects = ArticleQuerySet.as_manager() + objects = ProgramChildQuerySet.as_manager() + class Meta: verbose_name = _('Article') diff --git a/aircox/models/episode.py b/aircox/models/episode.py index 8826611..0412763 100644 --- a/aircox/models/episode.py +++ b/aircox/models/episode.py @@ -8,7 +8,7 @@ from django.utils.functional import cached_property from aircox import settings, utils -from .program import Program, InProgramQuerySet, \ +from .program import Program, ProgramChildQuerySet, \ BaseRerun, BaseRerunQuerySet from .page import Page, PageQuerySet @@ -16,18 +16,18 @@ from .page import Page, PageQuerySet __all__ = ['Episode', 'Diffusion', 'DiffusionQuerySet'] -class EpisodeQuerySet(PageQuerySet, InProgramQuerySet): - pass - - class Episode(Page): - program = models.ForeignKey( - Program, models.CASCADE, - verbose_name=_('program'), - ) - - objects = EpisodeQuerySet.as_manager() + objects = ProgramChildQuerySet.as_manager() detail_url_name = 'episode-detail' + item_template_name = 'aircox/episode_item.html' + + @property + def program(self): + return getattr(self.parent, 'program', None) + + @program.setter + def program(self, value): + self.parent = value class Meta: verbose_name = _('Episode') @@ -41,6 +41,8 @@ class Episode(Page): def save(self, *args, **kwargs): if self.cover is None: self.cover = self.program.cover + if self.parent is None: + raise ValueError('missing parent program') super().save(*args, **kwargs) @classmethod @@ -155,6 +157,8 @@ class Diffusion(BaseRerun): # help_text = _('use this input port'), # ) + item_template_name = 'aircox/diffusion_item.html' + class Meta: verbose_name = _('Diffusion') verbose_name_plural = _('Diffusions') diff --git a/aircox/models/page.py b/aircox/models/page.py index 648426b..6ccb5c8 100644 --- a/aircox/models/page.py +++ b/aircox/models/page.py @@ -46,6 +46,11 @@ class PageQuerySet(InheritanceQuerySet): def trash(self): return self.filter(status=Page.STATUS_TRASH) + def parent(self, parent=None, id=None): + """ Return pages having this parent. """ + return self.filter(parent=parent) if id is None else \ + self.filter(parent__id=id) + class Page(models.Model): """ Base class for publishable content """ @@ -58,6 +63,8 @@ class Page(models.Model): (STATUS_TRASH, _('trash')), ) + parent = models.ForeignKey('self', models.CASCADE, blank=True, null=True, + related_name='child_set') title = models.CharField(max_length=128) slug = models.SlugField(_('slug'), blank=True, unique=True) status = models.PositiveSmallIntegerField( @@ -74,7 +81,7 @@ class Page(models.Model): content = RichTextField( _('content'), blank=True, null=True, ) - date = models.DateTimeField(default=tz.now) + pub_date = models.DateTimeField(blank=True, null=True) featured = models.BooleanField( _('featured'), default=False, ) @@ -85,17 +92,19 @@ class Page(models.Model): objects = PageQuerySet.as_manager() detail_url_name = None - + item_template_name = 'aircox/page_item.html' def __str__(self): - return '{}: {}'.format(self._meta.verbose_name, - self.title or self.pk) + return '{}'.format(self.title or self.pk) def save(self, *args, **kwargs): # TODO: ensure unique slug if not self.slug: self.slug = slugify(self.title) - print(self.title, '--', self.slug) + if self.is_published and self.pub_date is None: + self.pub_date = tz.datetime.now() + elif not self.is_published: + self.pub_date = None super().save(*args, **kwargs) def get_absolute_url(self): diff --git a/aircox/models/program.py b/aircox/models/program.py index d1ed23f..13fee7b 100644 --- a/aircox/models/program.py +++ b/aircox/models/program.py @@ -23,7 +23,8 @@ from .station import Station logger = logging.getLogger('aircox') -__all__ = ['Program', 'ProgramQuerySet', 'Stream', 'Schedule'] +__all__ = ['Program', 'ProgramQuerySet', 'Stream', 'Schedule', + 'ProgramChildQuerySet', 'BaseRerun', 'BaseRerunQuerySet'] class ProgramQuerySet(PageQuerySet): @@ -49,15 +50,8 @@ class Program(Page): name if it does not exists. """ # explicit foreign key in order to avoid related name clashes - page = models.OneToOneField( - Page, models.CASCADE, - parent_link=True, related_name='program_page' - ) - station = models.ForeignKey( - Station, - verbose_name=_('station'), - on_delete=models.CASCADE, - ) + station = models.ForeignKey(Station, models.CASCADE, + verbose_name=_('station')) active = models.BooleanField( _('active'), default=True, @@ -146,10 +140,17 @@ class Program(Page): .update(path=Concat('path', Substr(F('path'), len(path_)))) -class InProgramQuerySet(models.QuerySet): - """ - Queryset for model having a ForeignKey field "program" to `Program`. - """ +class ProgramChildQuerySet(PageQuerySet): + def station(self, station=None, id=None): + return self.filter(parent__program__station=station) if id is None else \ + self.filter(parent__program__station__id=id) + + def program(self, program=None, id=None): + return self.parent(program, id) + + +class BaseRerunQuerySet(models.QuerySet): + """ Queryset for BaseRerun (sub)classes. """ def station(self, station=None, id=None): return self.filter(program__station=station) if id is None else \ self.filter(program__station__id=id) @@ -158,9 +159,6 @@ class InProgramQuerySet(models.QuerySet): return self.filter(program=program) if id is None else \ self.filter(program__id=id) - -class BaseRerunQuerySet(InProgramQuerySet): - """ Queryset for BaseRerun (sub)classes. """ def rerun(self): return self.filter(initial__isnull=False) diff --git a/aircox/models/signals.py b/aircox/models/signals.py index dfab8ef..351f6b1 100755 --- a/aircox/models/signals.py +++ b/aircox/models/signals.py @@ -93,7 +93,7 @@ def schedule_pre_delete(sender, instance, *args, **kwargs): @receiver(signals.post_delete, sender=Diffusion) def diffusion_post_delete(sender, instance, *args, **kwargs): - Episode.objects.filter(diffusion__isnull=True, content_isnull=True, + Episode.objects.filter(diffusion__isnull=True, content__isnull=True, sound__isnull=True) \ .delete() diff --git a/aircox/static/aircox/admin.css b/aircox/static/aircox/admin.css index c135ac1..fa1696a 100644 --- a/aircox/static/aircox/admin.css +++ b/aircox/static/aircox/admin.css @@ -16,6 +16,9 @@ width: 100%; margin: 1em 0em; } +ul.menu-list li { + list-style-type: none; } + @keyframes spinAround { from { transform: rotate(0deg); } @@ -7180,6 +7183,11 @@ label.panel-block { .has-background-transparent { background-color: transparent; } +.is-opacity-light { + opacity: 0.7; } + .is-opacity-light:hover { + opacity: 1; } + .navbar + .container { margin-top: 1em; } @@ -7192,6 +7200,29 @@ a.navbar-item.is-active { .navbar .navbar-dropdown { z-index: 2000; } +.navbar .navbar-split { + margin: 0.2em 0em; + margin-right: 1em; + padding-right: 1em; + border-right: 1px #b5b5b5 solid; + display: inline-block; } + +.navbar form { + margin: 0em; + padding: 0em; } + +.filters { + margin: 1em 0em; + background-color: transparent; + margin-bottom: 1em; } + .filters .title { + padding-right: 2em; + margin-right: 1em; + border-right: 1px #b5b5b5 solid; + font-size: 1.25rem; + color: #7a7a7a; + font-weight: 300; } + /* .navbar-brand img { min-height: 6em; diff --git a/aircox/static/aircox/admin.js b/aircox/static/aircox/admin.js index b4dde4f..81267d7 100644 --- a/aircox/static/aircox/admin.js +++ b/aircox/static/aircox/admin.js @@ -305,7 +305,7 @@ eval("// extracted by mini-css-extract-plugin\n\n//# sourceURL=webpack:///./asse /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; -eval("__webpack_require__.r(__webpack_exports__);\n//\n//\n//\n//\n//\n//\n\n\nconst splitReg = new RegExp(`,\\s*`, 'g');\n\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n data() {\n return {\n counts: {},\n }\n },\n\n methods: {\n update() {\n const items = this.$el.querySelectorAll('input[name=\"data\"]:checked')\n const counts = {};\n\n console.log(items)\n for(var item of items)\n if(item.value)\n for(var tag of item.value.split(splitReg))\n counts[tag.trim()] = (counts[tag.trim()] || 0) + 1;\n this.counts = counts;\n console.log('counts', this.counts)\n }\n },\n\n mounted() {\n this.$refs.form.addEventListener('change', () => this.update())\n this.update()\n }\n});\n\n\n//# sourceURL=webpack:///./assets/admin/statistics.vue?./node_modules/vue-loader/lib??vue-loader-options"); +eval("__webpack_require__.r(__webpack_exports__);\n//\n//\n//\n//\n//\n//\n\n\nconst splitReg = new RegExp(`,\\s*`, 'g');\n\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n data() {\n return {\n counts: {},\n }\n },\n\n methods: {\n update() {\n const items = this.$el.querySelectorAll('input[name=\"data\"]:checked')\n const counts = {};\n\n console.log(items)\n for(var item of items)\n if(item.value)\n for(var tag of item.value.split(splitReg))\n counts[tag.trim()] = (counts[tag.trim()] || 0) + 1;\n this.counts = counts;\n }\n },\n\n mounted() {\n this.$refs.form.addEventListener('change', () => this.update())\n this.update()\n }\n});\n\n\n//# sourceURL=webpack:///./assets/admin/statistics.vue?./node_modules/vue-loader/lib??vue-loader-options"); /***/ }), diff --git a/aircox/static/aircox/main.css b/aircox/static/aircox/main.css index a5530c6..5368918 100644 --- a/aircox/static/aircox/main.css +++ b/aircox/static/aircox/main.css @@ -7162,6 +7162,11 @@ label.panel-block { .has-background-transparent { background-color: transparent; } +.is-opacity-light { + opacity: 0.7; } + .is-opacity-light:hover { + opacity: 1; } + .navbar + .container { margin-top: 1em; } @@ -7174,6 +7179,29 @@ a.navbar-item.is-active { .navbar .navbar-dropdown { z-index: 2000; } +.navbar .navbar-split { + margin: 0.2em 0em; + margin-right: 1em; + padding-right: 1em; + border-right: 1px #b5b5b5 solid; + display: inline-block; } + +.navbar form { + margin: 0em; + padding: 0em; } + +.filters { + margin: 1em 0em; + background-color: transparent; + margin-bottom: 1em; } + .filters .title { + padding-right: 2em; + margin-right: 1em; + border-right: 1px #b5b5b5 solid; + font-size: 1.25rem; + color: #7a7a7a; + font-weight: 300; } + /* .navbar-brand img { min-height: 6em; diff --git a/aircox/templates/admin/aircox/statistics.html b/aircox/templates/admin/aircox/statistics.html index 572e64a..6dfa9b2 100644 --- a/aircox/templates/admin/aircox/statistics.html +++ b/aircox/templates/admin/aircox/statistics.html @@ -4,7 +4,9 @@ {% block content %}{{ block.super }} {# TODO: date subtitle #} - +
+ + + + + + +
{% endblock %} diff --git a/aircox/templates/admin/base.html b/aircox/templates/admin/base.html index 4382358..e66b147 100644 --- a/aircox/templates/admin/base.html +++ b/aircox/templates/admin/base.html @@ -46,7 +46,7 @@ {% trans "Articles" %} @@ -56,7 +56,7 @@ {% trans "Episodes" %} diff --git a/aircox/templates/aircox/article_detail.html b/aircox/templates/aircox/article_detail.html index b160431..dce7ec4 100644 --- a/aircox/templates/aircox/article_detail.html +++ b/aircox/templates/aircox/article_detail.html @@ -1,14 +1,14 @@ {% extends "aircox/page_detail.html" %} {% load i18n %} -{% block side_nav %} +{% block sidebar %} {{ block.super }} -{% if side_items %} +{% if sidebar_items %}

{% trans "Latest news" %}

- {% for object in side_items %} + {% for object in sidebar_items %} {% include "aircox/page_item.html" %} {% endfor %} diff --git a/aircox/templates/aircox/base.html b/aircox/templates/aircox/base.html index 90b8a3c..e06477f 100644 --- a/aircox/templates/aircox/base.html +++ b/aircox/templates/aircox/base.html @@ -3,6 +3,7 @@ Context: - cover: image cover - site: current website +- has_filters: display filter bar (using block "filters") {% endcomment %} @@ -71,19 +72,50 @@ Context: {% endblock %} + {% if has_filters %} + + {% endif %} + {% block main %}{% endblock main %} - {% if show_side_nav %} + {% if has_sidebar %} {% endif %} diff --git a/aircox/templates/aircox/diffusion_item.html b/aircox/templates/aircox/diffusion_item.html new file mode 100644 index 0000000..8b5d651 --- /dev/null +++ b/aircox/templates/aircox/diffusion_item.html @@ -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 %} + diff --git a/aircox/templates/aircox/diffusion_list.html b/aircox/templates/aircox/diffusion_list.html index eb76f57..78d60b4 100644 --- a/aircox/templates/aircox/diffusion_list.html +++ b/aircox/templates/aircox/diffusion_list.html @@ -1,5 +1,5 @@ {% extends "aircox/page.html" %} -{% load i18n aircox %} +{% load i18n aircox humanize %} {% block title %} {% with station.name as station %} @@ -9,17 +9,22 @@ {% block subtitle %}{{ date|date:"l d F Y" }}{% endblock %} -{% block main %}{{ block.super }} -
+{% block filters %} +{% with "diffusion-list" as url_name %} +{% include "aircox/widgets/dates_menu.html" %} +{% endwith %} +{% endblock %} +{% block main %}{{ block.super }} {% with True as hide_schedule %} -
+
{% for diffusion in object_list %} -
+ {# FIXME: opacity should work -- maybe hidden tz #} +
@@ -33,12 +38,10 @@
{% endwith %} +{% comment %} +{% endcomment %} -
{% endblock %} diff --git a/aircox/templates/aircox/log_item.html b/aircox/templates/aircox/log_item.html index 1694ee6..9cbae9b 100644 --- a/aircox/templates/aircox/log_item.html +++ b/aircox/templates/aircox/log_item.html @@ -11,9 +11,7 @@ for design review. {% if object|is_diffusion %} {% with object as diffusion %} - {% with diffusion.episode as object %} - {% include "aircox/episode_item.html" %} - {% endwith %} + {% include "aircox/diffusion_item.html" %} {% endwith %} {% else %} {% with object.track as object %} diff --git a/aircox/templates/aircox/log_list.html b/aircox/templates/aircox/log_list.html index 66dc1d2..be03e6a 100644 --- a/aircox/templates/aircox/log_list.html +++ b/aircox/templates/aircox/log_list.html @@ -10,14 +10,17 @@ {% block subtitle %}{{ date|date:"l d F Y" }}{% endblock %} +{% block filters %} +{% with "log-list" as url_name %} +{% include "aircox/widgets/dates_menu.html" %} +{% endwith %} +{% endblock %} {% block main %} -
- -
+
{#

{{ date }}

#} {% with True as hide_schedule %} - +
{% for object in object_list %}
@@ -37,13 +40,5 @@
{% endwith %}
- - - -
{% endblock %} diff --git a/aircox/templates/aircox/page_list.html b/aircox/templates/aircox/page_list.html index 2c63d1b..539a98c 100644 --- a/aircox/templates/aircox/page_list.html +++ b/aircox/templates/aircox/page_list.html @@ -2,63 +2,65 @@ {% load i18n aircox %} {% block title %} -{{ view.model|verbose_name:True|title }} +{% if not parent %}{{ view.model|verbose_name:True|title }} +{% else %} +{% with parent.title as title %} +{% blocktrans %}Publications of {{ title }}{% endblocktrans %} +{% endwith %} +{% endif %} {% endblock %} -{% block side_nav %} -{{ block.super }} - -{% if filter_categories|length != 1 %} -
-

{% trans "Filters" %}

-
- {% block list_filters %} -
-
- -
-
-
-
- {% for category in filter_categories %} - - {% endfor %} +{% block filters %} + + +
-{% endif %} + + {% endblock %} {% block main %} -
+
{% for object in object_list %} {% block list_object %} -{% include item_template_name %} +{% include object.item_template_name|default:item_template_name %} {% endblock %} {% endfor %}
diff --git a/aircox/templates/aircox/program_base.html b/aircox/templates/aircox/program_base.html index 14f6ad8..a3f5db2 100644 --- a/aircox/templates/aircox/program_base.html +++ b/aircox/templates/aircox/program_base.html @@ -1,30 +1,16 @@ {% extends "aircox/page_detail.html" %} {% load i18n %} -{% block side_nav %} -{{ block.super }} - -{% if side_items %} -
-

{% trans "Last shows" %}

- - {% for object in side_items %} - {% include "aircox/episode_item.html" %} - {% endfor %} - -
- -
-{% endif %} +{% block sidebar_title %} +{% with program.title as program %} +{% blocktrans %} Recently on {{ program }}{% endblocktrans %} +{% endwith %} {% endblock %} +{% block sidebar %} +{% with program as parent %} +{{ block.super }} +{% endwith %} +{% endblock %} + + diff --git a/aircox/templates/aircox/track_item.html b/aircox/templates/aircox/track_item.html index faf0972..ee1c3fe 100644 --- a/aircox/templates/aircox/track_item.html +++ b/aircox/templates/aircox/track_item.html @@ -4,9 +4,9 @@ Context: {% endcomment %} -{{ track.title }} +{{ object.title }} -— {{ track.artist }} -{% if track.info %}({{ track.info }}){% endif %} +— {{ object.artist }} +{% if object.info %}({{ object.info }}){% endif %} diff --git a/aircox/templates/aircox/widgets/dates_menu.html b/aircox/templates/aircox/widgets/dates_menu.html index f63e821..cbbb6c2 100644 --- a/aircox/templates/aircox/widgets/dates_menu.html +++ b/aircox/templates/aircox/widgets/dates_menu.html @@ -9,40 +9,35 @@ Context: An empty date results to a title or a separator {% endcomment %} -{% load i18n humanize %} +{% load i18n %} - + + + + diff --git a/aircox/urls.py b/aircox/urls.py index 805c4b9..6fbcf82 100755 --- a/aircox/urls.py +++ b/aircox/urls.py @@ -24,6 +24,8 @@ api = [ urls = [ + path(_(''), + views.DiffusionListView.as_view(), name='home'), path('api/', include(api)), # path('', views.PageDetailView.as_view(model=models.Article), @@ -35,15 +37,6 @@ urls = [ views.ArticleDetailView.as_view(), name='article-detail'), - path(_('programs/'), views.PageListView.as_view(model=models.Program), - name='program-list'), - path(_('programs//'), - views.ProgramDetailView.as_view(), name='program-detail'), - path(_('programs//episodes/'), - views.EpisodeListView.as_view(), name='episode-list'), - path(_('programs//articles/'), - views.ArticleListView.as_view(), name='article-list'), - path(_('episodes/'), views.EpisodeListView.as_view(), name='episode-list'), path(_('episodes//'), @@ -53,7 +46,23 @@ urls = [ path(_('week//'), views.DiffusionListView.as_view(), name='diffusion-list'), - path(_('logs/'), views.LogListView.as_view(), name='logs'), - path(_('logs//'), views.LogListView.as_view(), name='logs'), + path(_('logs/'), views.LogListView.as_view(), name='log-list'), + path(_('logs//'), views.LogListView.as_view(), name='log-list'), # 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//'), + views.ProgramDetailView.as_view(), name='program-detail'), + path(_('programs//episodes/'), + views.EpisodeListView.as_view(), name='episode-list'), + path(_('programs//articles/'), + views.ArticleListView.as_view(), name='article-list'), + path(_('programs//publications/'), + views.ProgramPageListView.as_view(), name='page-list'), + + ] diff --git a/aircox/views/__init__.py b/aircox/views/__init__.py index 4e8a364..ed44b56 100644 --- a/aircox/views/__init__.py +++ b/aircox/views/__init__.py @@ -5,5 +5,5 @@ from .base import BaseView from .episode import EpisodeDetailView, EpisodeListView, DiffusionListView from .log import LogListView from .page import PageDetailView, PageListView -from .program import ProgramDetailView +from .program import ProgramDetailView, ProgramPageListView diff --git a/aircox/views/admin.py b/aircox/views/admin.py index 6683723..49ec58a 100644 --- a/aircox/views/admin.py +++ b/aircox/views/admin.py @@ -49,6 +49,9 @@ class AdminSite(admin.AdminSite): path('tools/statistics/', self.admin_view(StatisticsView.as_view()), name='tools-stats'), + path('tools/statistics//', + self.admin_view(StatisticsView.as_view()), + name='tools-stats'), ] return urls diff --git a/aircox/views/article.py b/aircox/views/article.py index 8677f1d..e81cc1a 100644 --- a/aircox/views/article.py +++ b/aircox/views/article.py @@ -7,10 +7,10 @@ __all__ = ['ArticleDetailView', 'ArticleListView'] class ArticleDetailView(PageDetailView): - show_side_nav = True + has_sidebar = True model = Article - def get_side_queryset(self): + def get_sidebar_queryset(self): qs = Article.objects.select_related('cover') \ .filter(is_static=False) \ .order_by('-date') @@ -27,9 +27,7 @@ class ArticleListView(ParentMixin, PageListView): template_name = 'aircox/article_list.html' show_headline = True is_static = False - parent_model = Program - fk_parent = 'program' def get_queryset(self): return super().get_queryset().filter(is_static=self.is_static) diff --git a/aircox/views/base.py b/aircox/views/base.py index f293ca3..eb4c0f0 100644 --- a/aircox/views/base.py +++ b/aircox/views/base.py @@ -3,6 +3,7 @@ from django.http import Http404 from django.views.generic import DetailView, ListView from django.views.generic.base import TemplateResponseMixin, ContextMixin +from ..models import Page from ..utils import Redirect @@ -15,8 +16,10 @@ class BaseView(TemplateResponseMixin, ContextMixin): cover = None """ Page cover """ - show_side_nav = False + has_sidebar = True """ Show side navigation """ + has_filters = False + """ Show filters nav """ list_count = 5 """ Item count for small lists displayed on page. """ @@ -24,27 +27,29 @@ class BaseView(TemplateResponseMixin, ContextMixin): def station(self): return self.request.station - def get_queryset(self): - return super().get_queryset().station(self.station) + # def get_queryset(self): + # return super().get_queryset().station(self.station) - def get_side_queryset(self): + def get_sidebar_queryset(self): """ Return a queryset of items to render on the side nav. """ - return None + return Page.objects.select_subclasses().published() \ + .order_by('-pub_date') - def get_context_data(self, side_items=None, **kwargs): + def get_context_data(self, sidebar_items=None, **kwargs): kwargs.setdefault('station', self.station) kwargs.setdefault('cover', self.cover) + kwargs.setdefault('has_filters', self.has_filters) - show_side_nav = kwargs.setdefault('show_side_nav', self.show_side_nav) - if show_side_nav and side_items is None: - side_items = self.get_side_queryset() - side_items = None if side_items is None else \ - side_items[:self.list_count] + has_sidebar = kwargs.setdefault('has_sidebar', self.has_sidebar) + if has_sidebar and sidebar_items is None: + sidebar_items = self.get_sidebar_queryset() + sidebar_items = None if sidebar_items is None else \ + sidebar_items[:self.list_count] if not 'audio_streams' in kwargs: streams = self.station.audio_streams streams = streams and streams.split('\n') kwargs['audio_streams'] = streams - return super().get_context_data(side_items=side_items, **kwargs) + return super().get_context_data(sidebar_items=sidebar_items, **kwargs) diff --git a/aircox/views/episode.py b/aircox/views/episode.py index a4a8697..8e1aa1a 100644 --- a/aircox/views/episode.py +++ b/aircox/views/episode.py @@ -35,18 +35,14 @@ class EpisodeListView(ParentMixin, PageListView): model = Episode item_template_name = 'aircox/episode_item.html' show_headline = True - parent_model = Program - fk_parent = 'program' class DiffusionListView(GetDateMixin, BaseView, ListView): """ View for timetables """ model = Diffusion - - date = None - start = None - end = None + has_filters = True + redirect_date_url = 'diffusion-list' def get_date(self): date = super().get_date() @@ -56,19 +52,7 @@ class DiffusionListView(GetDateMixin, BaseView, ListView): return super().get_queryset().today(self.date).order_by('start') def get_context_data(self, **kwargs): - today = datetime.date.today() start = self.date - datetime.timedelta(days=self.date.weekday()) - dates = [ - (today, None), - (today - datetime.timedelta(days=1), None), - (today + datetime.timedelta(days=1), None), - (today - datetime.timedelta(days=7), _('next week')), - (today + datetime.timedelta(days=7), _('last week')), - (None, None) - ] + [ - (date, date.strftime('%A %d')) - for date in (start + datetime.timedelta(days=i) - for i in range(0, 7)) if date != today - ] + dates = [start + datetime.timedelta(days=i) for i in range(0, 7)] return super().get_context_data(date=self.date, dates=dates, **kwargs) diff --git a/aircox/views/log.py b/aircox/views/log.py index 3348cad..f61670e 100644 --- a/aircox/views/log.py +++ b/aircox/views/log.py @@ -48,6 +48,9 @@ class LogListView(BaseView, LogListMixin, ListView): Return list of logs for the provided date (from `kwargs` or `request.GET`, defaults to today). """ + redirect_date_url = 'log-list' + has_filters = True + def get_date(self): date, today = super().get_date(), datetime.date.today() return today if date is None else min(date, today) @@ -56,8 +59,7 @@ class LogListView(BaseView, LogListMixin, ListView): today = datetime.date.today() kwargs.update({ 'date': self.date, - 'dates': ((today - datetime.timedelta(days=i), None) - for i in range(0, 7)), + 'dates': (today - datetime.timedelta(days=i) for i in range(0, 7)), 'object_list': self.get_object_list(self.object_list), }) return super().get_context_data(**kwargs) diff --git a/aircox/views/mixins.py b/aircox/views/mixins.py index baabb90..6b56260 100644 --- a/aircox/views/mixins.py +++ b/aircox/views/mixins.py @@ -1,4 +1,6 @@ -from django.shortcuts import get_object_or_404 +import dateutil +from django.shortcuts import get_object_or_404, redirect +from django.urls import reverse from ..utils import str_to_date @@ -12,13 +14,18 @@ class GetDateMixin: `kwargs['date']` """ date = None + redirect_date_url = None def get_date(self): - if 'date' in self.request.GET: - return str_to_date(self.request.GET['date'], '-') - return self.kwargs['date'] if 'date' in self.kwargs else None + date = self.request.GET.get('date') + return str_to_date(date, '-') if date is not None else \ + self.kwargs['date'] if 'date' in self.kwargs else None def get(self, *args, **kwargs): + if self.redirect_date_url and self.request.GET.get('date'): + return redirect(self.redirect_date_url, + date=self.request.GET['date'].replace('-', '/')) + self.date = self.get_date() return super().get(*args, **kwargs) @@ -35,8 +42,6 @@ class ParentMixin: """ Url lookup argument """ parent_field = 'slug' """ Parent field for url lookup """ - fk_parent = 'page' - """ Page foreign key to the parent """ parent = None """ Parent page object """ @@ -54,8 +59,7 @@ class ParentMixin: def get_queryset(self): if self.parent is not None: - lookup = {self.fk_parent: self.parent} - return super().get_queryset().filter(**lookup) + return super().get_queryset().filter(parent=self.parent) return super().get_queryset() def get_context_data(self, **kwargs): diff --git a/aircox/views/page.py b/aircox/views/page.py index d64f161..1dcaa7f 100644 --- a/aircox/views/page.py +++ b/aircox/views/page.py @@ -1,6 +1,5 @@ from django.http import Http404, HttpResponse -from django.shortcuts import get_object_or_404 from django.utils.translation import ugettext_lazy as _ from django.views.generic import DetailView, ListView @@ -19,9 +18,11 @@ __all__ = ['PageDetailView', 'PageListView'] class PageListView(BaseView, ListView): template_name = 'aircox/page_list.html' item_template_name = 'aircox/page_item.html' + has_sidebar = True + has_filters = True + paginate_by = 20 show_headline = True - show_side_nav = True categories = None def get(self, *args, **kwargs): @@ -36,7 +37,7 @@ class PageListView(BaseView, ListView): # (by id) if self.categories: qs = qs.filter(category__slug__in=self.categories) - return qs.order_by('-date') + return qs.order_by('-pub_date') def get_categories_queryset(self): # TODO: use generic reverse field lookup @@ -56,6 +57,7 @@ class PageListView(BaseView, ListView): class PageDetailView(BaseView, DetailView): """ Base view class for pages. """ context_object_name = 'page' + has_filters = False def get_queryset(self): return super().get_queryset().select_related('cover', 'category') diff --git a/aircox/views/program.py b/aircox/views/program.py index 5506b82..e9698fc 100644 --- a/aircox/views/program.py +++ b/aircox/views/program.py @@ -1,11 +1,13 @@ +from django.db.models import Q from django.core.exceptions import ObjectDoesNotExist from django.shortcuts import get_object_or_404 -from aircox.models import Episode, Program +from ..models import Episode, Program, Page +from .mixins import ParentMixin from .page import PageDetailView, PageListView -__all__ = ['ProgramPageDetailView', 'ProgramDetailView'] +__all__ = ['ProgramPageDetailView', 'ProgramDetailView', 'ProgramPageListView'] class ProgramPageDetailView(PageDetailView): @@ -13,24 +15,25 @@ class ProgramPageDetailView(PageDetailView): Base view class for a page that is displayed as a program's child page. """ program = None - show_side_nav = True + has_sidebar = True list_count = 5 - def get_side_queryset(self): - return self.program.episode_set.published().order_by('-date') + def get_sidebar_queryset(self): + return super().get_sidebar_queryset().filter(parent=self.object) + + +class ProgramPageListView(ParentMixin, PageListView): + model = Page + parent_model = Program + queryset = Page.objects.select_subclasses() class ProgramDetailView(ProgramPageDetailView): model = Program - def get_articles_queryset(self): - return self.program.article_set.published().order_by('-date') - def get_context_data(self, **kwargs): self.program = kwargs.setdefault('program', self.object) - if 'articles' not in kwargs: - kwargs['articles'] = \ - self.get_articles_queryset()[:self.list_count] return super().get_context_data(**kwargs) + diff --git a/assets/admin/admin.scss b/assets/admin/admin.scss index 884f2ea..8b299d3 100644 --- a/assets/admin/admin.scss +++ b/assets/admin/admin.scss @@ -24,3 +24,9 @@ margin: 1em 0em; } + +ul.menu-list li { + list-style-type: none; +} + + diff --git a/assets/admin/statistics.vue b/assets/admin/statistics.vue index b19e6dd..bebbc65 100644 --- a/assets/admin/statistics.vue +++ b/assets/admin/statistics.vue @@ -26,7 +26,6 @@ export default { for(var tag of item.value.split(splitReg)) counts[tag.trim()] = (counts[tag.trim()] || 0) + 1; this.counts = counts; - console.log('counts', this.counts) } }, diff --git a/assets/public/styles.scss b/assets/public/styles.scss index 8ff69eb..1c4c987 100644 --- a/assets/public/styles.scss +++ b/assets/public/styles.scss @@ -5,6 +5,7 @@ $body-background-color: $light; @import "~bulma/bulma"; +//-- helpers/modifiers .is-fullwidth { width: 100%; } .is-fixed-bottom { position: fixed; @@ -18,6 +19,14 @@ $body-background-color: $light; background-color: transparent; } +.is-opacity-light { + opacity: 0.7; + &:hover { + opacity: 1; + } +} + +//-- navbar .navbar + .container { margin-top: 1em; } @@ -30,10 +39,44 @@ a.navbar-item.is-active { border-bottom: 1px grey solid; } -.navbar .navbar-dropdown { - z-index: 2000; +.navbar { + .navbar-dropdown { + z-index: 2000; + } + + .navbar-split { + margin: 0.2em 0em; + margin-right: 1em; + padding-right: 1em; + border-right: 1px $grey-light solid; + display: inline-block; + } + + form { + margin: 0em; + padding: 0em; + } } + +//-- filters +.filters { + margin: 1em 0em; + background-color: transparent; + margin-bottom: 1em; + + .title { + padding-right: 2em; + margin-right: 1em; + border-right: 1px $grey-light solid; + + font-size: $size-5; + color: $text-light; + font-weight: $weight-light; + } +} + + /* .navbar-brand img { min-height: 6em; diff --git a/requirements.txt b/requirements.txt index e76f7e2..2cd3016 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ Django>=2.2.0 djangorestframework>=3.9.4 +django-model-utils>=3.2.0 -dateutils>=0.6.6 watchdog>=0.8.3 psutil>=5.0.1 tzlocal>=1.4 @@ -12,7 +12,6 @@ django-filer>=1.5.0 django-ckeditor>=5.7.1 django-admin-sortable2>=0.7.2 django-content-editor>=1.4.2 - django-honeypot>=0.5.0 gunicorn>=19.6.0