From 3975c222c69d500150b051e7529fdf74d3b979ed Mon Sep 17 00:00:00 2001 From: bkfox Date: Fri, 22 Jul 2016 12:38:42 +0200 Subject: [PATCH] add date_list; LogsPage; ListPage --- cms/models.py | 403 ++++++++++++++---- .../cms/{index_page.html => list_page.html} | 2 +- cms/templates/cms/logs_page.html | 7 + cms/templates/cms/program_page.html | 8 +- cms/templates/cms/search.html | 10 - cms/templates/cms/snippets/date_list.html | 37 ++ cms/templates/cms/snippets/list.html | 50 +++ cms/templates/cms/snippets/list_item.html | 23 +- cms/templatetags/aircox_cms.py | 11 + cms/views.py | 10 - controllers/models.py | 42 ++ 11 files changed, 491 insertions(+), 112 deletions(-) rename cms/templates/cms/{index_page.html => list_page.html} (96%) create mode 100644 cms/templates/cms/logs_page.html delete mode 100644 cms/templates/cms/search.html create mode 100644 cms/templates/cms/snippets/date_list.html create mode 100644 cms/templatetags/aircox_cms.py diff --git a/cms/models.py b/cms/models.py index f5a7cc0..03f373a 100644 --- a/cms/models.py +++ b/cms/models.py @@ -1,3 +1,6 @@ +import datetime +import re +from enum import Enum, IntEnum from django.db import models from django.contrib.contenttypes.models import ContentType @@ -30,16 +33,32 @@ from taggit.models import TaggedItemBase import bleach import aircox.programs.models as programs +import aircox.controllers.models as controllers import aircox.cms.settings as settings +class ListItem: + """ + Generic normalized element to add item in lists that are not based + on Publication. + """ + title = '' + summary = '' + url = '' + cover = None + date = None + + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + self.specific = self + + @register_setting class WebsiteSettings(BaseSetting): logo = models.ForeignKey( 'wagtailimages.Image', verbose_name = _('logo'), - null=True, blank=True, - on_delete=models.SET_NULL, + null=True, blank=True, on_delete=models.SET_NULL, related_name='+', help_text = _('logo of the website'), ) @@ -91,8 +110,10 @@ class WebsiteSettings(BaseSetting): verbose_name = _('website settings') -class RelatedLink(Orderable): - parent = ParentalKey('Publication', related_name='related_links') +# +# Base +# +class BaseRelatedLink(Orderable): url = models.URLField( _('url'), help_text = _('URL of the link'), @@ -112,6 +133,9 @@ class RelatedLink(Orderable): help_text = _('text to display of the link'), ) + class Meta: + abstract = True + panels = [ FieldPanel('url'), FieldRowPanel([ @@ -121,6 +145,9 @@ class RelatedLink(Orderable): ] +# +# Publications +# @register_snippet class Comment(models.Model): publication = models.ForeignKey( @@ -170,6 +197,9 @@ class Comment(models.Model): return super().save(*args, **kwargs) +class RelatedLink(BaseRelatedLink): + parent = ParentalKey('Publication', related_name='related_links') + class PublicationTag(TaggedItemBase): content_object = ParentalKey('Publication', related_name='tagged_items') @@ -248,80 +278,6 @@ class Publication(Page): published = True, ).order_by('-date') - @classmethod - def get_queryset(cl, request, *args, - thread = None, context = {}, - **kwargs): - """ - Return a queryset from the request's GET parameters. Context - can be used to update relative informations. - - Parameters: - * type: ['program','diffusion','event'] type of the publication - * tag: tag to search for - * search: query to search in the publications - * thread: children of the thread passed in arguments only - * order: ['asc','desc'] sort ordering - * page: page number - - Context's fields: - * list_type: type of the items in the list (as Page subclass) - * tag_query: tag searched for - * search_query: search terms - * thread_query: thread - * paginator: paginator object - """ - if 'thread' in request.GET and thread: - qs = self.get_children() - context['thread_query'] = thread - else: - qs = cl.objects.all() - qs = qs.not_in_menu().live() - - # type - type = request.GET.get('type') - if type == 'program': - qs = qs.type(ProgramPage) - context['list_type'] = ProgramPage - elif type == 'diffusion': - qs = qs.type(DiffusionPage) - context['list_type'] = DiffusionPage - elif type == 'event': - qs = qs.type(EventPage) - context['list_type'] = EventPage - - # filter by tag - tag = request.GET.get('tag') - if tag: - context['tag_query'] = tag - qs = qs.filter(tags__name = tag) - - # search - search = request.GET.get('search') - if search: - context['search_query'] = search - qs = qs.search(search) - - # ordering - order = request.GET.get('order') - if order not in ('asc','desc'): - order = 'desc' - qs = qs.order_by( - ('-' if order == 'desc' else '') + 'first_published_at' - ) - - qs = self.get_queryset(request, *args, context, **kwargs) - if qs: - paginator = Paginator(qs, 30) - try: - qs = paginator.page('page') - except PageNotAnInteger: - qs = paginator.page(1) - except EmptyPage: - qs = parginator.page(paginator.num_pages) - context['paginator'] = paginator - return qs - def get_context(self, request, *args, **kwargs): from aircox.cms.forms import CommentForm context = super().get_context(request, *args, **kwargs) @@ -333,7 +289,7 @@ class Publication(Page): context['comment_form'] = CommentForm() if view == 'list': - context['object_list'] = self.get_queryset( + context['object_list'] = ListPage.get_queryset( request, *args, context = context, thread = self, **kwargs ) return context @@ -366,6 +322,7 @@ class ProgramPage(Publication): program = models.ForeignKey( programs.Program, verbose_name = _('program'), + related_name = 'page', on_delete=models.SET_NULL, blank=True, null=True, ) @@ -403,20 +360,22 @@ class ProgramPage(Publication): ), cover = self.cover, live = True, + date = diff.start, ) - diff.page_.date = diff.start return [ diff.page_ for diff in diffs if diff.page_.live ] - def next_diffs(self): + @property + def next(self): now = tz.now() diffs = programs.Diffusion.objects \ .filter(end__gte = now, program = self.program) \ .order_by('start').prefetch_related('page') return self.diffs_to_page(diffs) - def prev_diffs(self): + @property + def prev(self): now = tz.now() diffs = programs.Diffusion.objects \ .filter(end__lte = now, program = self.program) \ @@ -471,6 +430,32 @@ class DiffusionPage(Publication): InlinePanel('tracks', label=_('Tracks')) ] + @classmethod + def as_item(cl, diff): + """ + Return a DiffusionPage or ListItem from a Diffusion + """ + if diff.page.all().count(): + item = diff.page.live().first() + else: + item = ListItem( + title = '{}, {}'.format( + diff.program.name, diff.date.strftime('%d %B %Y') + ), + cover = (diff.program.page.count() and \ + diff.program.page.first().cover) or '', + live = True, + date = diff.start, + ) + + if diff.initial: + item.info = _('Rerun of %(date)s') % { + 'date': diff.initial.start.strftime('%A %d') + } + diff.css_class = 'diffusion' + + return item + def save(self, *args, **kwargs): if self.diffusion: self.first_published_at = self.diffusion.start @@ -528,6 +513,260 @@ class EventPage(Publication): super().save(*args, **kwargs) +# +# Indexes +# +class BaseDateList(models.Model): + nav_days = models.SmallIntegerField( + _('navigation days count'), + default = 7, + help_text = _('number of days to display in the navigation header ' + 'when we use dates') + ) + nav_per_week = models.BooleanField( + _('navigation per week'), + default = False, + help_text = _('if selected, show dates navigation per weeks instead ' + 'of show days equally around the current date') + ) + + @staticmethod + def str_to_date(date): + """ + Parse a string and return a regular date or None. + Format is either "YYYY/MM/DD" or "YYYY-MM-DD" or "YYYYMMDD" + """ + try: + exp = r'(?P[0-9]{4})(-|\/)?(?P[0-9]{1,2})(-|\/)?' \ + r'(?P[0-9]{1,2})' + date = re.match(exp, date).groupdict() + return datetime.date( + year = int(date['year']), month = int(date['month']), + day = int(date['day']) + ) + except: + return None + + def get_date_context(self, date, date_max = None): + """ + Return a dict that can be added to the context to be used by + a date_list. + """ + if not date: + date = tz.now().today() + + if date_max: + date = min(date, date_max) + + # dates + if date_max == date: + first = self.nav_days - 1 + elif self.nav_per_week: + first = date.weekday() + else: + first = int((self.nav_days - 1) / 2) + first = date - tz.timedelta(days = first) + dates = [ first + tz.timedelta(days=i) + for i in range(0, self.nav_days) ] + + # next/prev weeks/date bunch + next = date + tz.timedelta(days=self.nav_days) + prev = date - tz.timedelta(days=self.nav_days) + + if date_max: + dates = [ date for date in dates if date <= date_max ] + next = min(next, date_max) + if next in dates: + next = None + prev = min(prev, date_max) + + # context dict + return { + 'nav_dates': { + 'date': date, + 'next': next, + 'prev': prev, + 'dates': dates, + } + } + + class Meta: + abstract = True + + +class ListPage(Page): + """ + Page for simple lists, query is done though request' GET fields. + Look at get_queryset for more information. + """ + summary = models.TextField( + _('summary'), + blank = True, null = True, + help_text = _('some short description if you want to, just for fun'), + ) + + @classmethod + def get_queryset(cl, request, *args, + thread = None, context = {}, + **kwargs): + """ + Return a queryset from the request's GET parameters. Context + can be used to update relative informations. + + This function can be used by other views if needed + + Parameters: + * type: ['program','diffusion','event'] type of the publication + * tag: tag to search for + * search: query to search in the publications + * thread: children of the thread passed in arguments only + * order: ['asc','desc'] sort ordering + * page: page number + + Context's fields: + * object_list: the final queryset + * list_type: type of the items in the list (as Page subclass) + * tag_query: tag searched for + * search_query: search terms + * thread_query: thread + * paginator: paginator object + """ + if 'thread' in request.GET and thread: + qs = thread.get_children().not_in_menu() + context['thread_query'] = thread + else: + qs = Publication.objects.all() + qs = qs.live() + + # ordering + order = request.GET.get('order') + if order not in ('asc','desc'): + order = 'desc' + qs = qs.order_by( + ('-' if order == 'desc' else '') + 'first_published_at' + ) + + # type + type = request.GET.get('type') + if type == 'program': + qs = qs.type(ProgramPage) + context['list_type'] = ProgramPage + elif type == 'diffusion': + qs = qs.type(DiffusionPage) + context['list_type'] = DiffusionPage + elif type == 'event': + qs = qs.type(EventPage) + context['list_type'] = EventPage + + # filter by tag + tag = request.GET.get('tag') + if tag: + context['tag_query'] = tag + qs = qs.filter(tags__name = tag) + + # search + search = request.GET.get('search') + if search: + context['search_query'] = search + qs = qs.search(search) + + # paginator + if qs: + paginator = Paginator(qs, 30) + try: + qs = paginator.page('page') + except PageNotAnInteger: + qs = paginator.page(1) + except EmptyPage: + qs = parginator.page(paginator.num_pages) + context['paginator'] = paginator + context['object_list'] = qs + return qs + + def get_context(self, request, *args, **kwargs): + context = super().get_context(request, *args, **kwargs) + qs = self.get_queryset(request, context=context) + context['object_list'] = qs + return context + + +class LogsPage(BaseDateList,Page): + summary = models.TextField( + _('summary'), + blank = True, null = True, + help_text = _('some short description if you want to, just for fun'), + ) + station = models.ForeignKey( + controllers.Station, + verbose_name = _('station'), + null = True, + on_delete=models.SET_NULL, + help_text = _('(required for logs) the station on which the logs ' + 'happened') + ) + max_days = models.IntegerField( + _('maximum days'), + default=15, + help_text = _('maximum days in the past allowed to be shown. ' + '0 means no limit') + ) + + + content_panels = [ + FieldPanel('title'), + MultiFieldPanel([ + FieldPanel('station'), + FieldPanel('max_days'), + ], heading=_('Configuration')), + ] + + + def as_item(cl, log): + """ + Return a log object as a DiffusionPage or ListItem. + Supports: Log/Track, Diffusion + """ + if type(log) == programs.Diffusion: + return DiffusionPage.as_item(log) + return ListItem( + title = '{artist} -- {title}'.format( + artist = log.related.artist, + title = log.related.title, + ), + summary = log.related.info, + date = log.date, + info = '♫', + css_class = 'track' + ) + + def get_queryset(self, request, context): + if 'date' in request.GET: + date = request.GET.get('date') + date = self.str_to_date(date) + + if date and self.max_days: + date = max( + tz.now().date() - tz.timedelta(days=self.max_days), + date + ) + else: + date = tz.now().date() + context.update(self.get_date_context(date, date_max=tz.now().date())) + + r = [] + for date in context['nav_dates']['dates']: + logs = self.station.get_on_air(date) + logs = [ self.as_item(log) for log in logs ] + r.append((date, logs)) + return r + + def get_context(self, request, *args, **kwargs): + context = super().get_context(request, *args, **kwargs) + qs = self.get_queryset(request, context) + context['object_list'] = qs + return context + + # # Menus and Sections # diff --git a/cms/templates/cms/index_page.html b/cms/templates/cms/list_page.html similarity index 96% rename from cms/templates/cms/index_page.html rename to cms/templates/cms/list_page.html index 81697a6..dd5c3d3 100644 --- a/cms/templates/cms/index_page.html +++ b/cms/templates/cms/list_page.html @@ -34,7 +34,7 @@ {% endif %} {% with list_paginator=paginator %} -{% include "cms/list.html" %} +{% include "cms/snippets/list.html" %} {% endwith %} {% endblock %} diff --git a/cms/templates/cms/logs_page.html b/cms/templates/cms/logs_page.html new file mode 100644 index 0000000..08c4a3d --- /dev/null +++ b/cms/templates/cms/logs_page.html @@ -0,0 +1,7 @@ +{% extends "cms/base_site.html" %} +{# generic page to display list of articles #} + +{% block content %} +{% include "cms/snippets/date_list.html" %} +{% endblock %} + diff --git a/cms/templates/cms/program_page.html b/cms/templates/cms/program_page.html index 3cf7721..3798b81 100644 --- a/cms/templates/cms/program_page.html +++ b/cms/templates/cms/program_page.html @@ -34,21 +34,21 @@ {% block page_nav_extras %} {% if page.program.active %} -{% with object_list=page.next_diffs %} +{% with object_list=page.next %} {% if object_list %}

{% trans "Next Diffusions" %}

- {% include "cms/list.html" %} + {% include "cms/snippets/list.html" %}
{% endif %} {% endwith %} {% endif %}{# program.active #} -{% with object_list=page.prev_diffs %} +{% with object_list=page.prev %} {% if object_list %}

{% trans "Previous Diffusions" %}

- {% include "cms/list.html" %} + {% include "cms/snippets/list.html" %}
{% endif %} {% endwith %} diff --git a/cms/templates/cms/search.html b/cms/templates/cms/search.html deleted file mode 100644 index b488178..0000000 --- a/cms/templates/cms/search.html +++ /dev/null @@ -1,10 +0,0 @@ -{% extends "cms/base_site.html" %} -{% load i18n %} - - -{% block title %} - -{% endblock %} - - - diff --git a/cms/templates/cms/snippets/date_list.html b/cms/templates/cms/snippets/date_list.html new file mode 100644 index 0000000..fc56d66 --- /dev/null +++ b/cms/templates/cms/snippets/date_list.html @@ -0,0 +1,37 @@ +{% load i18n %} + +{# FIXME: get current complete URL #} +
+{% if nav_dates %} + +{% endif %} + +{% for day, list in object_list %} +
    + {# you might like to hide it by default -- this more for sections #} +

    {{ day|date:'l d F' }}

    + {% with object_list=list list_date_format="H:i" %} + {% for item in list %} + {% include "cms/snippets/list_item.html" %} + {% endfor %} + {% endwith %} +
+{% endfor %} +
+ diff --git a/cms/templates/cms/snippets/list.html b/cms/templates/cms/snippets/list.html index 328fcd3..422ce30 100644 --- a/cms/templates/cms/snippets/list.html +++ b/cms/templates/cms/snippets/list.html @@ -1,9 +1,59 @@ +{% load i18n %} +{% load aircox_cms %} +
+
    {% for page in object_list %} {% with item=page.specific %} {% include "cms/snippets/list_item.html" %} {% endwith %} {% endfor %} +
+ +{# we use list_paginator to avoid conflicts when there are multiple lists #} +{% if list_paginator and list_paginator.num_pages > 1 %} + +{% endif %} +
+ diff --git a/cms/templates/cms/snippets/list_item.html b/cms/templates/cms/snippets/list_item.html index e728d6d..840f9db 100644 --- a/cms/templates/cms/snippets/list_item.html +++ b/cms/templates/cms/snippets/list_item.html @@ -1,15 +1,28 @@ +{% comment %} +Configurable item to be put in a list. Support standard Publication or +ListItem instance. + +Options: +* item: item to render. Fields: title, summary, cover, url, date, info, css_class +* list_date_format: format passed to the date filter instead of default one +{% endcomment %} + {% load wagtailimages_tags %} - + {% image item.cover fill-64x64 class="cover item_cover" %}

{{ item.title }}

-
{{ item.summary }}
- {% if not item.show_in_menus %} - {% if item.date %} + {% if item.summary %}
{{ item.summary }}
{% endif %} + {% if not item.show_in_menus and item.date %} + {% with date_format=list_date_format|default:'l d F, H:i' %} + {% endwith %} {% endif %} + {% if item.info %} + {{ item.info|safe }} {% endif %}
diff --git a/cms/templatetags/aircox_cms.py b/cms/templatetags/aircox_cms.py new file mode 100644 index 0000000..d6a18d7 --- /dev/null +++ b/cms/templatetags/aircox_cms.py @@ -0,0 +1,11 @@ +from django import template +register = template.Library() + +@register.filter(name='around') +def around(page_num, n): + """ + Return a range of value around a given number. + """ + return range(page_num-n, page_num+n+1) + + diff --git a/cms/views.py b/cms/views.py index e1ef2dc..2800278 100644 --- a/cms/views.py +++ b/cms/views.py @@ -1,12 +1,2 @@ from django.shortcuts import render -from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger -from aircox.cms.models import * - - -def index_page(request): - context = {} - if ('tag' or 'search') in request.GET: - qs = Publication.get_queryset(request, context = context) - - return render(request, 'index_page.html', context) diff --git a/controllers/models.py b/controllers/models.py index 541658c..61e568e 100644 --- a/controllers/models.py +++ b/controllers/models.py @@ -12,6 +12,7 @@ sources that are used to generate the audio stream: - **master**: main output """ import os +import datetime import logging from enum import Enum, IntEnum @@ -160,6 +161,47 @@ class Station(programs.Nameable): ) return qs.order_by('date') + def get_on_air(self, date = None): + """ + Return a list of what should have normally been on air at the + given date, ordered descending on the diffusion time + + The list contains: + - track logs: for the streamed programs; + - diffusion: for the scheduled diffusions; + """ + # TODO: argument to get sound instead of tracks + date = date or tz.now().date() + if date > datetime.date.today(): + return [] + + logs = Log.get_for(model = programs.Track) \ + .filter(date__contains = date) \ + .order_by('date') + diffs = programs.Diffusion.objects.get_at(date) \ + .filter(type = programs.Diffusion.Type.normal) \ + .order_by('start') + + # mix up + items = [] + prev_diff = None + for diff in diffs: + logs_ = logs.filter(date__gt = prev_diff.end, + date__lt = diff.start) \ + if prev_diff else \ + logs.filter(date__lt = diff.start) + prev_diff = diff + items.extend(logs_) + items.append(diff) + + # last logs + if prev_diff: + logs_ = logs.filter(date__gt = prev_diff.end) + items.extend(logs_) + return reversed(items) + + + def save(self, make_sources = True, *args, **kwargs): """ * make_sources: if the model has not been yet saved, generate