aircox/aircox_cms/models/sections.py
2018-02-09 18:11:04 +01:00

651 lines
20 KiB
Python

from enum import IntEnum
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.template import Template, Context
from django.utils.translation import ugettext as _, ugettext_lazy
from django.utils.functional import cached_property
from django.urls import reverse
from modelcluster.models import ClusterableModel
from modelcluster.fields import ParentalKey
from wagtail.wagtailadmin.edit_handlers import *
from wagtail.wagtailimages.edit_handlers import ImageChooserPanel
from wagtail.wagtailcore.models import Page
from wagtail.wagtailcore.fields import RichTextField
from wagtail.wagtailsnippets.models import register_snippet
import aircox.models
from aircox_cms.models.lists import *
from aircox_cms.views.components import Component, ExposedData
from aircox_cms.utils import related_pages_filter
@register_snippet
class Region(ClusterableModel):
"""
Region is a container of multiple items of different types
that are used to render extra content related or not the current
page.
A section has an assigned position in the page, and can be restrained
to a given type of page.
"""
name = models.CharField(
_('name'),
max_length=32,
blank = True, null = True,
help_text=_('name of this section (not displayed)'),
)
position = models.CharField(
_('position'),
max_length=16,
blank = True, null = True,
help_text = _('name of the template block in which the section must '
'be set'),
)
order = models.IntegerField(
_('order'),
default = 100,
help_text = _('order of rendering, the higher the latest')
)
model = models.ForeignKey(
ContentType,
verbose_name = _('model'),
blank = True, null = True,
help_text=_('this section is displayed only when the current '
'page or publication is of this type'),
limit_choices_to = related_pages_filter,
)
page = models.ForeignKey(
Page,
verbose_name = _('page'),
blank = True, null = True,
help_text=_('this section is displayed only on this page'),
)
panels = [
MultiFieldPanel([
FieldPanel('name'),
FieldPanel('position'),
FieldPanel('model'),
FieldPanel('page'),
], heading=_('General')),
# InlinePanel('items', label=_('Region Items')),
]
@classmethod
def get_sections_at (cl, position, page = None):
"""
Return a queryset of sections that are at the given position.
Filter out Region that are not for the given page.
"""
qs = Region.objects.filter(position = position)
if page:
qs = qs.filter(
models.Q(page__isnull = True) |
models.Q(page = page)
)
qs = qs.filter(
models.Q(model__isnull = True) |
models.Q(
model = ContentType.objects.get_for_model(page).pk
)
)
return qs.order_by('order','pk')
def add_item(self, item):
"""
Add an item to the section. Automatically save the item and
create the corresponding SectionPlace.
"""
item.section = self
item.save()
def render(self, request, page = None, context = None, *args, **kwargs):
return ''.join([
item.specific.render(request, page, context, *args, **kwargs)
for item in self.items.all().order_by('order','pk')
])
def __str__(self):
return '{}: {}'.format(self.__class__.__name__, self.name or self.pk)
@register_snippet
class Section(Component, models.Model):
"""
Section is a widget configurable by user that can be rendered inside
Regions.
"""
template_name = 'aircox_cms/sections/section.html'
section = ParentalKey(Region, related_name='items')
order = models.IntegerField(
_('order'),
default = 100,
help_text = _('order of rendering, the higher the latest')
)
real_type = models.CharField(
max_length=32,
blank = True, null = True,
)
title = models.CharField(
_('title'),
max_length=32,
blank = True, null = True,
)
show_title = models.BooleanField(
_('show title'),
default = False,
help_text=_('if set show a title at the head of the section'),
)
css_class = models.CharField(
_('CSS class'),
max_length=64,
blank = True, null = True,
help_text=_('section container\'s "class" attribute')
)
template_name = 'aircox_cms/sections/item.html'
panels = [
MultiFieldPanel([
FieldPanel('section'),
FieldPanel('title'),
FieldPanel('show_title'),
FieldPanel('order'),
FieldPanel('css_class'),
], heading=_('General')),
]
# TODO make it reusable
@cached_property
def specific(self):
"""
Return a downcasted version of the model if it is from another
model, or itself
"""
if not self.real_type or type(self) != Section:
return self
return getattr(self, self.real_type)
def save(self, *args, **kwargs):
if type(self) != Section and not self.real_type:
self.real_type = type(self).__name__.lower()
return super().save(*args, **kwargs)
def __str__(self):
return '{}: {}'.format(
(self.real_type or 'section item').replace('section','section '),
self.title or self.pk
)
class SectionRelativeItem(Section):
is_related = models.BooleanField(
_('is related'),
default = False,
help_text=_(
'if set, section is related to the page being processed '
'e.g rendering a list of links will use thoses of the '
'publication instead of an assigned one.'
)
)
class Meta:
abstract=True
panels = Section.panels.copy()
panels[-1] = MultiFieldPanel(
panels[-1].children + [ FieldPanel('is_related') ],
heading = panels[-1].heading
)
def related_attr(self, page, attr):
"""
Return an attribute from the given page if self.is_related,
otherwise retrieve the attribute from self.
"""
return self.is_related and hasattr(page, attr) \
and getattr(page, attr)
@register_snippet
class SectionText(Section):
template_name = 'aircox_cms/sections/text.html'
body = RichTextField()
panels = Section.panels + [
FieldPanel('body'),
]
def get_context(self, request, page):
from wagtail.wagtailcore.rich_text import expand_db_html
context = super().get_context(request, page)
context['content'] = expand_db_html(self.body)
return context
@register_snippet
class SectionImage(SectionRelativeItem):
class ResizeMode(IntEnum):
max = 0x00
min = 0x01
crop = 0x02
image = models.ForeignKey(
'wagtailimages.Image',
verbose_name = _('image'),
related_name='+',
blank=True, null=True,
help_text=_(
'If this item is related to the current page, this image will '
'be used only when the page has not a cover'
)
)
width = models.SmallIntegerField(
_('width'),
blank=True, null=True,
help_text=_('if set and > 0, sets a maximum width for the image'),
)
height = models.SmallIntegerField(
_('height'),
blank=True, null=True,
help_text=_('if set 0 and > 0, sets a maximum height for the image'),
)
resize_mode = models.SmallIntegerField(
verbose_name = _('resize mode'),
choices = [ (int(y), _(x)) for x,y in ResizeMode.__members__.items() ],
default = int(ResizeMode.max),
help_text=_('if the image is resized, set the resizing mode'),
)
panels = Section.panels + [
ImageChooserPanel('image'),
MultiFieldPanel([
FieldPanel('width'),
FieldPanel('height'),
FieldPanel('resize_mode'),
], heading=_('Resizing'))
]
cache = ""
def get_filter(self):
return \
'original' if not (self.height or self.width) else \
'width-{}'.format(self.width) if not self.height else \
'height-{}'.format(self.height) if not self.width else \
'{}-{}x{}'.format(
self.get_resize_mode_display(),
self.width, self.height
)
def ensure_cache(self, image):
"""
Ensure that we have a generated image and that it is put in cache.
We use this method since generating dynamic signatures don't generate
static images (and we need it).
"""
# Note: in order to put the generated image in db, we first need a way
# to get save events from related page or image.
if self.cache:
return self.cache
if self.width or self.height:
template = Template(
'{% load wagtailimages_tags %}\n' +
'{{% image source {filter} as img %}}'.format(
filter = self.get_filter()
) +
'<img src="{{ img.url }}">'
)
context = Context({
"source": image
})
self.cache = template.render(context)
else:
self.cache = '<img src="{}"/>'.format(image.file.url)
return self.cache
def get_context(self, request, page):
from wagtail.wagtailimages.views.serve import generate_signature
context = super().get_context(request, page)
image = self.related_attr(page, 'cover') or self.image
if not image:
return context
context['content'] = self.ensure_cache(image)
return context
@register_snippet
class SectionLinkList(ClusterableModel, Section):
template_name = 'aircox_cms/sections/link_list.html'
panels = Section.panels + [
InlinePanel('links', label=_('Links')),
]
@register_snippet
class SectionLink(RelatedLinkBase, Component):
"""
Render a link to a page or a given url.
Can either be used standalone or in a SectionLinkList
"""
template_name = 'aircox_cms/snippets/link.html'
parent = ParentalKey(
'SectionLinkList', related_name = 'links',
null = True
)
def __str__(self):
return 'link: {} #{}'.format(
self.text or (self.page and self.page.title) or self.title,
self.pk
)
@register_snippet
class SectionList(BaseList, SectionRelativeItem):
"""
This one is quite badass, but needed: render a list of pages
using given parameters (cf. BaseList).
If focus_available, the first article in the list will be the last
article with a focus, and will be rendered in a bigger size.
"""
template_name = 'aircox_cms/sections/list.html'
# TODO/FIXME: focus, quid?
# TODO: logs in menu show headline???
url_text = models.CharField(
_('text of the url'),
max_length=32,
blank = True, null = True,
help_text = _('use this text to display an URL to the complete '
'list. If empty, no link is displayed'),
)
panels = SectionRelativeItem.panels + [
FieldPanel('url_text'),
] + BaseList.panels
def get_context(self, request, page):
import aircox_cms.models as cms
if self.is_related and not self.related:
# set current page if there is not yet a related page only
self.related = page
context = BaseList.get_context(self, request, paginate = False)
if not context['object_list'].count():
self.hide = True
return {}
context.update(SectionRelativeItem.get_context(self, request, page))
if self.url_text:
self.related = self.related and self.related.specific
target = None
if self.related and hasattr(self.related, 'get_list_page'):
target = self.related.get_list_page()
if not target:
settings = cms.WebsiteSettings.for_site(request.site)
target = settings.list_page
context['url'] = self.to_url(page = target) + '&view=list'
return context
SectionList._meta.get_field('count').default = 5
@register_snippet
class SectionLogsList(Section):
template_name = 'aircox_cms/sections/logs_list.html'
station = models.ForeignKey(
aircox.models.Station,
verbose_name = _('station'),
null = True,
on_delete=models.SET_NULL,
help_text = _('(required) the station on which the logs happened')
)
count = models.SmallIntegerField(
_('count'),
default = 5,
help_text = _('number of items to display in the list (max 100)'),
)
class Meta:
verbose_name = _('list of logs')
verbose_name_plural = _('lists of logs')
panels = Section.panels + [
FieldPanel('station'),
FieldPanel('count'),
]
@staticmethod
def as_item(log):
"""
Return a log object as a DiffusionPage or ListItem.
Supports: Log/Track, Diffusion
"""
from aircox_cms.models import DiffusionPage
if log.diffusion:
return DiffusionPage.as_item(log.diffusion)
track = log.track
return ListItem(
title = '{artist} -- {title}'.format(
artist = track.artist,
title = track.title,
),
headline = track.info,
date = log.date,
info = '',
css_class = 'track'
)
def get_context(self, request, page):
context = super().get_context(request, page)
context['object_list'] = [
self.as_item(item)
for item in self.station.on_air(count = min(self.count, 100))
]
return context
@register_snippet
class SectionTimetable(Section,DatedBaseList):
template_name = 'aircox_cms/sections/timetable.html'
class Meta:
verbose_name = _('Section: Timetable')
verbose_name_plural = _('Sections: Timetable')
station = models.ForeignKey(
aircox.models.Station,
verbose_name = _('station'),
help_text = _('(required) related station')
)
target = models.ForeignKey(
'aircox_cms.TimetablePage',
verbose_name = _('timetable page'),
blank = True, null = True,
help_text = _('select a timetable page used to show complete timetable'),
)
nav_visible = models.BooleanField(
_('show date navigation'),
default = True,
help_text = _('if checked, navigation dates will be shown')
)
# TODO: put in multi-field panel of DatedBaseList
panels = Section.panels + DatedBaseList.panels + [
MultiFieldPanel([
FieldPanel('nav_visible'),
FieldPanel('station'),
FieldPanel('target'),
], heading=_('Timetable')),
]
def get_queryset(self, context):
from aircox_cms.models import DiffusionPage
diffs = []
for date in context['nav_dates']['dates']:
items = aircox.models.Diffusion.objects.at(self.station, date)
items = [ DiffusionPage.as_item(item) for item in items ]
diffs.append((date, items))
return diffs
def get_context(self, request, page):
context = super().get_context(request, page)
context.update(self.get_date_context())
context['object_list'] = self.get_queryset(context)
context['target'] = self.target
if not self.nav_visible:
del context['nav_dates']['dates'];
return context
@register_snippet
class SectionPublicationInfo(Section):
template_name = 'aircox_cms/sections/publication_info.html'
class Meta:
verbose_name = _('Section: publication\'s info')
verbose_name_plural = _('Sections: publication\'s info')
@register_snippet
class SectionSearchField(Section):
template_name = 'aircox_cms/sections/search_field.html'
default_text = models.CharField(
_('default text'),
max_length=32,
default=_('search'),
help_text=_('text to display when the search field is empty'),
)
class Meta:
verbose_name = _('Section: search field')
verbose_name_plural = _('Sections: search field')
panels = Section.panels + [
FieldPanel('default_text'),
]
@register_snippet
class SectionPlaylist(Section):
"""
User playlist. Can be used to add sounds in it -- there should
only be one for the moment.
"""
class Track(ExposedData):
"""
Class exposed to Javascript playlist manager as Track.
"""
fields = {
'name': 'name',
'embed': 'embed',
'duration': lambda e, o:
o.duration.hour * 3600 + o.duration.minute * 60 +
o.duration.second
,
'duration_str': lambda e, o:
(str(o.duration.hour) + '"' if o.duration.hour else '') +
str(o.duration.minute) + "'" + str(o.duration.second)
,
'sources': lambda e, o: [ o.url() ],
'detail_url':
lambda e, o: o.diffusion and hasattr(o.diffusion, 'page') \
and o.diffusion.page.url
,
'cover':
lambda e, o: o.diffusion and hasattr(o.diffusion, 'page') \
and o.diffusion.page.icon
,
}
user_playlist = models.BooleanField(
_('user playlist'),
default = False,
help_text = _(
'if set, this playlist is to be editable by the user'
)
)
single_mode = models.BooleanField(
_('single_mode'),
default = False,
help_text = _(
'enable single mode by default on this playlist'
)
)
tracks = None
template_name = 'aircox_cms/sections/playlist.html'
panels = Section.panels + [
FieldPanel('user_playlist'),
FieldPanel('single_mode'),
]
def __init__(self, *args, sounds = None, tracks = None, page = None, **kwargs):
"""
Init playlist section. If ``sounds`` is given initialize playlist
tracks with it. If ``page`` is given use it for Track infos
related to a page (cover, detail_url, ...)
"""
self.tracks = (tracks or []) + [
self.Track(object = sound, detail_url = page and page.url,
cover = page and page.icon)
for sound in sounds or []
]
super().__init__(*args, **kwargs)
def get_context(self, request, page):
context = super().get_context(request, page)
context.update({
'is_default': self.user_playlist,
'modifiable': self.user_playlist,
'storage_key': self.user_playlist and str(self.pk),
'tracks': self.tracks
})
if not self.user_playlist and not self.tracks:
self.hide = True
return context
@register_snippet
class SectionPlayer(Section):
"""
Radio stream player.
"""
template_name = 'aircox_cms/sections/playlist.html'
live_title = models.CharField(
_('live title'),
max_length = 32,
help_text = _('text to display when it plays live'),
)
streams = models.TextField(
_('audio streams'),
help_text = _('one audio stream per line'),
)
class Meta:
verbose_name = _('Section: Player')
panels = Section.panels + [
FieldPanel('live_title'),
FieldPanel('streams'),
]
def get_context(self, request, page):
context = super().get_context(request, page)
context['tracks'] = [SectionPlaylist.Track(
name = self.live_title,
sources = self.streams.split('\r\n'),
data_url = 'https://aircox.radiocampus.be/aircox/on_air', # reverse('aircox.on_air'),
interval = 10,
run = True,
)]
return context