sections rendering + make view for existing section's items

This commit is contained in:
bkfox
2016-07-24 19:49:15 +02:00
parent 5ab731e866
commit 90021bc9f0
13 changed files with 395 additions and 94 deletions

View File

@ -396,7 +396,7 @@ class DiffusionPage(Publication):
Return a DiffusionPage or ListItem from a Diffusion
"""
if diff.page.all().count():
item = diff.page.live().first()
item = diff.page.all().first()
else:
item = ListItem(
title = '{}, {}'.format(
@ -493,7 +493,7 @@ class ListPage(Page):
def get_context(self, request, *args, **kwargs):
context = super().get_context(request, *args, **kwargs)
qs = ListBase.get_queryset_from_request(request, context=context)
qs = ListBase.from_request(request, context=context)
context['object_list'] = qs
return context

View File

@ -1,11 +1,11 @@
import datetime
import re
from enum import Enum, IntEnum
from enum import IntEnum
from django.db import models
from django.utils.translation import ugettext as _, ugettext_lazy
from django.utils import timezone as tz
from django.core.urlresolvers import reverse
from django.template.loader import render_to_string
from django.contrib.contenttypes.models import ContentType
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
@ -23,6 +23,8 @@ from wagtail.wagtailcore.utils import camelcase_to_underscore
from wagtail.wagtailsnippets.edit_handlers import SnippetChooserPanel
from wagtail.wagtailsnippets.models import register_snippet
from wagtail.wagtailimages.utils import generate_signature
# tags
from modelcluster.models import ClusterableModel
from modelcluster.fields import ParentalKey
@ -30,27 +32,28 @@ from modelcluster.tags import ClusterTaggableManager
from taggit.models import TaggedItemBase
def related_pages_filter():
def related_pages_filter(reset_cache=False):
"""
Return a dict that can be used to filter foreignkey to pages'
subtype declared in aircox.cms.models.
Note: the filter is calculated at the first call of the function.
This value is stored in cache, but it is possible to reset the
cache using the `reset_cache` parameter.
"""
if hasattr(related_pages_filter, 'filter'):
return related_pages_filter.filter
if not reset_cache and hasattr(related_pages_filter, 'cache'):
return related_pages_filter.cache
import aircox.cms.models as cms
import inspect
related_pages_filter.filter = {
'model__in': (name.lower() for name, member in
related_pages_filter.cache = {
'model__in': list(name.lower() for name, member in
inspect.getmembers(cms,
lambda x: inspect.isclass(x) and issubclass(x, Page)
)
if member != Page
),
}
return related_pages_filter.filter
return related_pages_filter.cache
class ListItem:
@ -113,6 +116,21 @@ class RelatedLinkBase(Orderable):
], heading=_('link'))
]
def as_dict(self):
"""
Return compiled values from parameters as dict with
'url', 'icon', 'text'
"""
if self.page:
url, text = self.page.url, self.page.title
else:
url, text = self.url, self.url
return {
'url': url,
'text': self.text or text,
'icon': self.icon
}
class ListBase(models.Model):
"""
@ -125,96 +143,128 @@ class ListBase(models.Model):
before_related = 0x03,
after_related = 0x04,
filter_date = models.SmallIntegerField(
date_filter = models.SmallIntegerField(
verbose_name = _('filter by date'),
choices = [ (int(y), _(x.replace('_', ' ')))
for x,y in DateFilter.__members__.items() ],
blank = True, null = True,
)
filter_model = models.ForeignKey(
model = models.ForeignKey(
ContentType,
verbose_name = _('filter by type'),
blank = True, null = True,
help_text = _('if set, select only elements that are of this type'),
limit_choices_to = related_pages_filter,
)
filter_related = models.ForeignKey(
'Publication',
related = models.ForeignKey(
Page,
verbose_name = _('filter by a related publication'),
blank = True, null = True,
help_text = _('if set, select children or siblings of this publication'),
help_text = _('if set, select children or siblings related to this page'),
)
related_sibling = models.BooleanField(
verbose_name = _('related is a sibling'),
siblings = models.BooleanField(
verbose_name = _('select siblings of related'),
default = False,
help_text = _('if selected select related publications that are '
'siblings of this one'),
)
sort_asc = models.BooleanField(
verbose_name = _('order ascending'),
asc = models.BooleanField(
verbose_name = _('ascending order'),
default = True,
help_text = _('if selected sort list in the ascending order by date')
)
panels = [
MultiFieldPanel([
FieldPanel('filter_model'),
FieldPanel('filter_related'),
FieldPanel('related_sibling'),
FieldPanel('model'),
PageChooserPanel('related'),
FieldPanel('siblings'),
], heading=_('filters')),
MultiFieldPanel([
FieldPanel('filter_date'),
FieldPanel('sort_asc'),
FieldPanel('date_filter'),
FieldPanel('asc'),
], heading=_('sorting'))
]
class Meta:
abstract = True
@classmethod
def get_base_queryset(cl, filter_date = None, filter_model = None,
filter_related = None, related_siblings = None,
sort_asc = True):
def get_queryset(self):
"""
Get queryset based on the arguments. This class is intended to be
reusable by other classes if needed.
"""
from aircox.cms.models import Publication
related = self.related and self.related.specific()
# model
if filter_model:
qs = filter_model.objects.all()
if self.model:
qs = self.model.model_class().objects.all()
else:
qs = Publication.objects.all()
qs = qs.live().not_in_section()
qs = qs.live().not_in_menu()
# related
if filter_related:
if related_siblings:
qs = qs.sibling_of(filter_related)
if related:
if self.siblings:
qs = qs.sibling_of(related)
else:
qs = qs.descendant_of(filter_related)
qs = qs.descendant_of(related)
date = filter_related.date
if filter_date == cl.DateFilter.before_related:
date = self.related.date if hasattr('date', related) else \
self.related.first_published_at
if self.date_filter == self.DateFilter.before_related:
qs = qs.filter(date__lt = date)
elif filter_date == cl.DateFilter.after_related:
elif self.date_filter == self.DateFilter.after_related:
qs = qs.filter(date__gte = date)
# date
else:
date = tz.now()
if filter_date == cl.DateFilter.previous:
if self.date_filter == self.DateFilter.previous:
qs = qs.filter(date__lt = date)
elif filter_date == cl.DateFilter.next:
elif self.date_filter == self.DateFilter.next:
qs = qs.filter(date__gte = date)
# sort
if sort_asc:
if self.asc:
return qs.order_by('date', 'pk')
return qs.order_by('-date', '-pk')
def to_url(self, list_page = None, **kwargs):
"""
Return a url parameters from self. Extra named parameters are used
to override values of self or add some to the parameters.
If there is related field use it to get the page, otherwise use the
given list_page or the first ListPage it finds.
"""
import aircox.cms.models as models
params = {
'date_filter': self.get_date_filter_display(),
'model': self.model and self.model.model,
'asc': self.asc,
'related': self.related,
'siblings': self.siblings,
}
params.update(kwargs)
page = params.get('related') or list_page or \
models.ListPage.objects.all().first()
if params.get('related'):
params['related'] = True
params = '&'.join([
key if value == True else '{}={}'.format(key, value)
for key, value in params.items()
if value
])
return page.url + '?' + params
@classmethod
def get_queryset_from_request(cl, request, *args,
related = None, context = {},
**kwargs):
def from_request(cl, request, related = None, context = None,
*args, **kwargs):
"""
Return a queryset from the request's GET parameters. Context
can be used to update relative informations.
@ -222,10 +272,12 @@ class ListBase(models.Model):
This function can be used by other views if needed
Parameters:
* date_filter: one of DateFilter attribute's key.
* model: ['program','diffusion','event'] type of the publication
* asc: if present, sort ascending instead of descending
* related: children of the thread passed in arguments only
* siblings: sibling of the related instead of children
* tag: tag to search for
* search: query to search in the publications
* page: page number
@ -236,17 +288,29 @@ class ListBase(models.Model):
arguments passed to ListBase.get_base_queryset
* paginator: paginator object
"""
def set(key, value):
if context is not None:
context[key] = value
date_filter = request.GET.get('date_filter')
model = request.GET.get('model')
kwargs = {
'filter_model': ProgramPage if model == 'program' else \
DiffusionPage if model == 'diffusion' else \
EventPage if model == 'event' else None,
'filter_related': 'related' in request.GET and related,
'related_siblings': 'siblings' in request.GET,
'sort_asc': 'asc' in request.GET,
'date_filter':
int(getattr(cl.DateFilter, date_filter))
if date_filter and hasattr(cl.DateFilter, date_filter)
else None,
'model':
ProgramPage if model == 'program' else
DiffusionPage if model == 'diffusion' else
EventPage if model == 'event' else None,
'related': 'related' in request.GET and related,
'siblings': 'siblings' in request.GET,
'asc': 'asc' in request.GET,
}
qs = cl.get_base_queryset(**kwargs)
base_list = cl(**{ k:v for k,v in kwargs.items() if v })
qs = base_list.get_queryset()
# filter by tag
tag = request.GET.get('tag')
@ -260,7 +324,7 @@ class ListBase(models.Model):
kwargs['terms'] = search
qs = qs.search(search)
context['list_selector'] = kwargs
set('list_selector', kwargs)
# paginator
if qs:
@ -271,8 +335,8 @@ class ListBase(models.Model):
qs = paginator.page(1)
except EmptyPage:
qs = parginator.page(paginator.num_pages)
context['paginator'] = paginator
context['object_list'] = qs
set('paginator', paginator)
set('object_list', qs)
return qs
@ -405,10 +469,26 @@ class Section(ClusterableModel):
def __str__(self):
return '{}: {}'.format(self.__class__.__name__, self.name or self.pk)
@classmethod
def get_sections_at (cl, position, page = None):
"""
Return a queryset of sections that are at the given position.
Filter out Section that are not for the given page.
"""
qs = Section.objects.filter(position = position)
if page:
qs = qs.filter(
models.Q(model__isnull = True) |
models.Q(
model = ContentType.objects.get_for_model(page).pk
)
)
return qs
def render(self, request, page = None, *args, **kwargs):
return ''.join([
place.item.render(request, page, *args, **kwargs)
for place in self.places
place.item.specific().render(request, page, *args, **kwargs)
for place in self.places.all()
])
@ -419,26 +499,42 @@ class SectionPlace(Orderable):
verbose_name=_('item')
)
panels = [
SnippetChooserPanel('item'),
FieldPanel('model'),
]
panels = [ SnippetChooserPanel('item'), ]
def __str__(self):
return '{}: {}'.format(self.__class__.__name__, self.title or self.pk)
class SectionItemMeta(models.base.ModelBase):
"""
Metaclass for SectionItem, assigning needed values such as `template`.
It needs to load the item's template if the section uses the default
one, and throw error if there is an error in the template.
"""
def __new__(cls, name, bases, attrs):
cl = super().__new__(name, bases, attrs)
from django.template.loader import get_template
from django.template import TemplateDoesNotExist
cl = super().__new__(cls, name, bases, attrs)
if not 'template' in attrs:
attrs['template'] = 'cms/sections/{}.html' \
.format(camelcase_to_underscore(name))
cl.template = '{}/sections/{}.html'.format(
cl._meta.app_label,
camelcase_to_underscore(name),
)
if name != 'SectionItem':
try:
get_template(cl.template)
except TemplateDoesNotExist:
cl.template = 'cms/sections/section_item.html'
return cl
@register_snippet
class SectionItem(models.Model):
class SectionItem(models.Model,metaclass=SectionItemMeta):
"""
Base class for a section item.
"""
real_type = models.CharField(
max_length=32,
blank = True, null = True,
@ -453,8 +549,8 @@ class SectionItem(models.Model):
default = False,
help_text=_('if set show a title at the head of the section'),
)
related = models.BooleanField(
_('related'),
is_related = models.BooleanField(
_('is related'),
default = False,
help_text=_('if set, section is related to the current publication. '
'e.g rendering a list related to it instead of to all '
@ -492,17 +588,34 @@ class SectionItem(models.Model):
return super().save(*args, **kwargs)
def get_context(self, request, page, *args, **kwargs):
"""
Default context attributes:
* self: section being rendered
* page: current page being rendered
* request: request used to render the current page
Other context attributes usable in the default template:
* content: **safe string** set as content of the section
"""
return {
'self': self,
'page': page,
'request': request,
}
def render(self, request, page, *args, **kwargs):
"""
Render the section. Page is the current publication being rendered.
Rendering is similar to pages, using 'template' attribute set
by default to the app_label/sections/model_name_snake_case.html
If the default template is not found, use SectionItem's one,
that can have a context attribute 'content' that is used to render
content.
"""
return render_to_string(
request, self.template,
self.template,
self.get_context(request, *args, page=page, **kwargs)
)
@ -520,22 +633,78 @@ class SectionText(SectionItem):
FieldPanel('body'),
]
def get_context(self, request, page, *args, **kwargs):
from wagtail.wagtailcore.rich_text import expand_db_html
context = super().get_context(request, page, *args, **kwargs)
context['content'] = expand_db_html(self.body)
return context
@register_snippet
class SectionImage(SectionItem):
class ResizeMode(IntEnum):
max = 0x00
min = 0x01
crop = 0x02
image = models.ForeignKey(
'wagtailimages.Image',
verbose_name = _('image'),
related_name='+',
)
width = models.SmallIntegerField(
_('width'),
blank=True, null=True,
help_text=_('if set and > 0, set a maximum width for the image'),
)
height = models.SmallIntegerField(
_('height'),
blank=True, null=True,
help_text=_('if set 0 and > 0, set 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 = SectionItem.panels + [
ImageChooserPanel('image'),
MultiFieldPanel([
FieldPanel('width'),
FieldPanel('height'),
FieldPanel('resize_mode'),
], heading=_('Resizing'))
]
def get_context(self, request, page, *args, **kwargs):
context = super().get_context(request, page, *args, **kwargs)
image = self.image
if self.width or self.height:
filter_spec = \
'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
)
filter_spec = (image.id, filter_spec)
url = reverse(
'wagtailimages_serve',
args=(generate_signature(*filter_spec), *filter_spec)
)
else:
url = image.file.url
context['content'] = '<img src="{}"/>'.format(url)
return context
@register_snippet
class SectionLink(RelatedLinkBase,SectionItem):
class SectionLink(RelatedLinkBase, SectionItem):
"""
Render a link to a page or a given url.
Can either be used standalone or in a SectionLinkList
"""
parent = ParentalKey('SectionLinkList', related_name='links',
@ -544,21 +713,83 @@ class SectionLink(RelatedLinkBase,SectionItem):
@register_snippet
class SectionLinkList(SectionItem,ClusterableModel):
class SectionLinkList(SectionItem, ClusterableModel):
"""
Render a list of links. If related to the current page, print
the page's links otherwise, the assigned link list.
Note: assign the link's class to the <a> tag if there is some.
"""
panels = SectionItem.panels + [
InlinePanel('links', label=_('links'))
]
def get_context(self, request, page, *args, **kwargs):
context = super().get_context(*args, **kwargs)
links = \
self.links if not self.is_related else \
page.related_links if hasattr(page, 'related_links') else \
None
context['object_list'] = links
return context
@register_snippet
class SectionPublicationList(ListBase,SectionItem):
class SectionList(ListBase, SectionItem):
"""
This one is quite badass, but needed: render a list of pages
using given parameters (cf. ListBase).
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.
"""
focus_available = models.BooleanField(
_('focus available'),
default = False,
help_text = _('if true, highlight the first focused article found')
)
panels = SectionItem.panels + [ FieldPanel('focus_available') ] +\
ListBase.panels
count = models.SmallIntegerField(
_('count'),
default = 5,
help_text = _('number of items to display in the list'),
)
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, does not print an address'),
)
panels = SectionItem.panels + [
MultiFieldPanel([
FieldPanel('focus_available'),
FieldPanel('count'),
FieldPanel('url_text'),
], heading=_('Rendering')),
] + ListBase.panels
def get_context(self, request, page, *args, **kwargs):
from aircox.cms.models import Publication
context = super().get_context(request, page, *args, **kwargs)
qs = self.get_queryset()
if self.focus_available:
focus = qs.type(Publication).filter(focus = True).first()
if focus:
focus.css_class = \
focus.css_class + ' focus' if focus.css_class else 'focus'
qs = qs.exclude(focus.page)
else:
focus = None
pages = qs[:self.count - (focus != None)]
context['focus'] = focus
context['object_list'] = pages
if self.url_text:
context['url'] = self.to_url(
list_page = self.is_related and page
)
return context

View File

@ -1,7 +1,11 @@
{% load staticfiles %}
{% load i18n %}
{% load wagtailimages_tags %}
{% load wagtailsettings_tags %}
{% load aircox_cms %}
{% get_settings %}
<html>
@ -26,7 +30,7 @@
<div class="middle">
<nav class="left">
<img src="{{ settings.cms.WebsiteSettings.logo.file.url }}" class="logo">
{% render_sections position="menu_left" %}
</nav>
<main>

View File

@ -0,0 +1,8 @@
<div class="section_item {{ self.css_class }}">
{% block title %}
{% if self.show_title %}<h2>{{ self.title }}</h2>{% endif %}
{% endblock %}
{% block content %}{{ content|safe }}{% endblock %}
</div>

View File

@ -0,0 +1,13 @@
{% extends "cms/sections/section_item.html" %}
{% load wagtailimages_tags %}
{% block content %}
{% with link=self.as_dict %}
<a href="{{ link.url }}">
{% if link.icon %}{% image link.icon fill-32x32 class="icon" %}{% endif %}
{{ link.text }}
</a>
{% endwith %}
{% endblock %}

View File

@ -0,0 +1,16 @@
{% extends "cms/sections/section_item.html" %}
{% load wagtailimages_tags %}
{% block content %}
{% for item in object_list %}
{% with link=item.as_dict %}
<a href="{{ link.url }}"
{% if item.css_class %}class="{{ item.css_class }}"{% endif %}>
{% if link.icon %}{% image link.icon fill-32x32 class="icon" %}{% endif %}
{{ link.text }}
</a>
{% endwith %}
{% endfor %}
{% endblock %}

View File

@ -0,0 +1,17 @@
{% extends "cms/sections/section_item.html" %}
{% block content %}
{% if focus %}
{% with item=focus item_big_cover=True %}
{% include "cms/snippets/list_item.html" %}
{% endwith %}
{% endif %}
{% include "cms/snippets/list.html" %}
{% if url %}
<nav><a href="{{ url }}">{{ self.url_text }}</nav>
{% endif %}
{% endblock %}

View File

@ -26,7 +26,7 @@
{% if day == nav_dates.date %}class="today"{% endif %}>
{# you might like to hide it by default -- this more for sections #}
<h2>{{ day|date:'l d F' }}</h2>
{% with object_list=list list_date_format="H:i" %}
{% with object_list=list item_date_format="H:i" %}
{% for item in list %}
{% include "cms/snippets/list_item.html" %}
{% endfor %}

View File

@ -4,14 +4,19 @@ 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
* item_date_format: format passed to the date filter instead of default one
* item_big_cover: cover should is big instead of thumbnail (width: 600)
{% endcomment %}
{% load wagtailimages_tags %}
<a {% if item.url %}href="{{ item.url }}" {% endif %}
class="item page_item{% if item.css_class %}{{ item.css_class }}{% endif %}">
{% image item.cover fill-64x64 class="cover item_cover" %}
{% if item_big_cover %}
{% image item.cover max-600 class="cover item_cover" %}
{% else %}
{% image item.cover fill-64x64 class="cover item_cover" %}
{% endif %}
<h3>{{ item.title }}</h3>
{% if item.summary %}<div class="summary">{{ item.summary }}</div>{% endif %}
{% if not item.show_in_menus and item.date %}

View File

@ -1,7 +1,7 @@
from django import template
from django.db.models import Q
from django.utils.safestring import mark_safe
import aircox.cms.sections as sections
from aircox.cms.sections import Section
register = template.Library()
@ -13,21 +13,15 @@ def around(page_num, n):
return range(page_num-n, page_num+n+1)
@register.simple_tag(takes_context=True)
def render_section(position = None, section = None, context = None):
def render_sections(context, position = None):
"""
If section is not given, render all sections at the given
position (filter out base on page models' too, cf. Section.model).
Render all sections at the given position (filter out base on page
models' too, cf. Section.model).
"""
request = context.get('request')
page = context.get('page')
if section:
return section.render(request, page=page)
sections = Section.objects \
.filter( Q(model__isnull = True) | Q(model = type(page)) ) \
.filter(position = position)
return '\n'.join(
section.render(request, page=page) for section in sections
)
return mark_safe(''.join(
section.render(request, page=page)
for section in Section.get_sections_at(position, page)
))

View File

@ -14,7 +14,7 @@ sources that are used to generate the audio stream:
import os
import datetime
import logging
from enum import Enum, IntEnum
from enum import IntEnum
from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey

View File

@ -1,3 +1,16 @@
This file is used as a reminder, can be used as crappy documentation too.
# conventions
## coding style
* name of classes relative to a class:
- metaclass: `class_name + 'Meta'`
- base classes: `class_name + 'Base'`
## aircox.cms
* icons: cropped to 32x32
* cover in list items: cropped 64x64
# TODO:
- general:

View File

@ -2,7 +2,7 @@ import datetime
import os
import shutil
import logging
from enum import Enum, IntEnum
from enum import IntEnum
from django.db import models
from django.template.defaultfilters import slugify