move sections relative items into cms/sections.py file

This commit is contained in:
bkfox 2016-07-22 17:46:42 +02:00
parent eae1b9dca0
commit 8906ba0919
4 changed files with 414 additions and 381 deletions

View File

@ -1,10 +1,6 @@
import datetime import datetime
import re
from enum import Enum, IntEnum
from django.db import models from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib import messages from django.contrib import messages
from django.utils import timezone as tz from django.utils import timezone as tz
@ -18,13 +14,12 @@ from wagtail.wagtailcore.fields import RichTextField
from wagtail.wagtailimages.edit_handlers import ImageChooserPanel from wagtail.wagtailimages.edit_handlers import ImageChooserPanel
from wagtail.wagtailadmin.edit_handlers import FieldPanel, FieldRowPanel, \ from wagtail.wagtailadmin.edit_handlers import FieldPanel, FieldRowPanel, \
MultiFieldPanel, InlinePanel, PageChooserPanel, StreamFieldPanel MultiFieldPanel, InlinePanel, PageChooserPanel, StreamFieldPanel
from wagtail.wagtailsearch import index
# snippets # snippets
from wagtail.wagtailsnippets.edit_handlers import SnippetChooserPanel
from wagtail.wagtailsnippets.models import register_snippet from wagtail.wagtailsnippets.models import register_snippet
# tags # tags
from modelcluster.models import ClusterableModel
from modelcluster.fields import ParentalKey from modelcluster.fields import ParentalKey
from modelcluster.tags import ClusterTaggableManager from modelcluster.tags import ClusterTaggableManager
from taggit.models import TaggedItemBase from taggit.models import TaggedItemBase
@ -36,21 +31,7 @@ import aircox.programs.models as programs
import aircox.controllers.models as controllers import aircox.controllers.models as controllers
import aircox.cms.settings as settings import aircox.cms.settings as settings
from aircox.cms.sections import *
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 @register_setting
@ -110,208 +91,6 @@ class WebsiteSettings(BaseSetting):
verbose_name = _('website settings') verbose_name = _('website settings')
#
# Base
#
class BaseRelatedLink(Orderable):
url = models.URLField(
_('url'),
help_text = _('URL of the link'),
)
icon = models.ForeignKey(
'wagtailimages.Image',
verbose_name = _('icon'),
null=True, blank=True,
on_delete=models.SET_NULL,
related_name='+',
help_text = _('icon to display before the url'),
)
title = models.CharField(
_('title'),
max_length = 64,
null = True, blank=True,
help_text = _('text to display of the link'),
)
class Meta:
abstract = True
panels = [
FieldPanel('url'),
FieldRowPanel([
FieldPanel('title'),
ImageChooserPanel('icon'),
]),
]
class BaseList(models.Model):
"""
Generic list
"""
class DateFilter(IntEnum):
none = 0x00
previous = 0x01
next = 0x02
before_related = 0x03,
after_related = 0x04,
filter_date = 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(
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 = {
'publication__isnull': False,
}
)
filter_related = models.ForeignKey(
'Publication',
verbose_name = _('filter by a related publication'),
blank = True, null = True,
help_text = _('if set, select children or siblings of this publication'),
)
related_sibling = models.BooleanField(
verbose_name = _('related is a sibling'),
default = False,
help_text = _('if selected select related publications that are '
'siblings of this one'),
)
sort_asc = models.BooleanField(
verbose_name = _('order ascending'),
default = True,
help_text = _('if selected sort list in the ascending order by date')
)
@classmethod
def get_base_queryset(cl, filter_date = None, filter_model = None,
filter_related = None, related_siblings = None,
sort_asc = True):
"""
Get queryset based on the arguments. This class is intended to be
reusable by other classes if needed.
"""
# model
if filter_model:
qs = filter_model.objects.all()
else:
qs = Publication.objects.all()
qs = qs.live().not_in_menu()
# related
if filter_related:
if related_siblings:
qs = qs.sibling_of(filter_related)
else:
qs = qs.descendant_of(filter_related)
date = filter_related.date
if filter_date == cl.DateFilter.before_related:
qs = qs.filter(date__lt = date)
elif filter_date == cl.DateFilter.after_related:
qs = qs.filter(date__gte = date)
# date
else:
date = tz.now()
if filter_date == cl.DateFilter.previous:
qs = qs.filter(date__lt = date)
elif filter_date == cl.DateFilter.next:
qs = qs.filter(date__gte = date)
# sort
if sort_asc:
return qs.order_by('date', 'pk')
return qs.order_by('-date', '-pk')
class BaseDatedList(models.Model):
"""
List that display items per days. Renders a navigation menu on the
top.
"""
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')
)
class Meta:
abstract = True
panels = [
MultiFieldPanel([
FieldPanel('nav_days'),
FieldPanel('nav_per_week'),
], heading=_('Navigation')),
]
@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<year>[0-9]{4})(-|\/)?(?P<month>[0-9]{1,2})(-|\/)?' \
r'(?P<day>[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_nav_dates(self, date):
"""
Return a list of dates availables for the navigation
"""
if self.nav_per_week:
first = date.weekday()
else:
first = int((self.nav_days - 1) / 2)
first = date - tz.timedelta(days = first)
return [ first + tz.timedelta(days=i)
for i in range(0, self.nav_days) ]
def get_date_context(self, date):
"""
Return a dict that can be added to the context to be used by
a date_list.
"""
today = tz.now().today()
if not date:
date = today
# next/prev weeks/date bunch
dates = self.get_nav_dates(date)
next = date + tz.timedelta(days=self.nav_days)
prev = date - tz.timedelta(days=self.nav_days)
# context dict
return {
'nav_dates': {
'date': date,
'next': next,
'prev': prev,
'dates': dates,
}
}
# #
# Publications # Publications
# #
@ -433,6 +212,11 @@ class Publication(Page):
FieldPanel('publish_as'), FieldPanel('publish_as'),
FieldPanel('allow_comments'), FieldPanel('allow_comments'),
] ]
search_fields = [
index.SearchField('body'),
index.FilterField('live'),
index.FilterField('show_in_menus'),
]
@property @property
def recents(self): def recents(self):
@ -463,7 +247,7 @@ class Publication(Page):
if view == 'list': if view == 'list':
context['object_list'] = ListPage.get_queryset( context['object_list'] = ListPage.get_queryset(
request, *args, context = context, thread = self, **kwargs request, context = context, related = self
) )
return context return context
@ -699,73 +483,9 @@ class ListPage(Page):
help_text = _('add an extra description for this list') help_text = _('add an extra description for this list')
) )
@classmethod
def get_queryset(cl, request, *args,
related = 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:
* 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
Context's fields:
* object_list: the final queryset
* list_selector: dict of { 'tag_query', 'search_query' } plus
arguments passed to BaseList.get_base_queryset
* paginator: paginator object
"""
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,
}
qs = BaseList.get_base_queryset(**kwargs)
# filter by tag
tag = request.GET.get('tag')
if tag:
kwargs['tag_query'] = tag
qs = qs.filter(tags__name = tag)
# search
search = request.GET.get('search')
if search:
kwargs['search_query'] = search
qs = qs.search(search)
context['list_selector'] = kwargs
# 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): def get_context(self, request, *args, **kwargs):
context = super().get_context(request, *args, **kwargs) context = super().get_context(request, *args, **kwargs)
qs = self.get_queryset(request, context=context) qs = BaseList.get_queryset_from_request(request, context=context)
context['object_list'] = qs context['object_list'] = qs
return context return context
@ -898,92 +618,4 @@ class TimetablePage(DatedListPage):
return diffs return diffs
#
# Menus
#
@register_snippet
class Menu(ClusterableModel):
name = models.CharField(
_('name'),
max_length=32,
blank = True, null = True,
help_text=_('name of this menu (not displayed)'),
)
css_class = models.CharField(
_('CSS class'),
max_length=64,
blank = True, null = True,
help_text=_('menu container\'s "class" attribute')
)
related = models.ForeignKey(
ContentType,
blank = True, null = True,
help_text=_('this menu is displayed only for this model')
)
position = models.CharField(
_('position'),
max_length=16,
blank = True, null = True,
help_text = _('name of the template block in which the menu must '
'be set'),
)
panels = [
MultiFieldPanel([
FieldPanel('name'),
FieldPanel('css_class'),
], heading=_('General')),
MultiFieldPanel([
FieldPanel('related'),
FieldPanel('position'),
], heading=_('Position')),
InlinePanel('menu_items', label=_('menu items')),
]
@register_snippet
class MenuItem(models.Model):
menu = ParentalKey(Menu, related_name='menu_items')
real_type = models.CharField(
max_length=32,
blank = True, null = True,
)
title = models.CharField(
_('title'),
max_length=32,
blank = True, null = True,
)
css_class = models.CharField(
_('CSS class'),
max_length=64,
blank = True, null = True,
help_text=_('menu container\'s "class" attribute')
)
panels = [
MultiFieldPanel([
FieldPanel('name'),
FieldPanel('css_class'),
], heading=_('General')),
]
def specific(self):
"""
Return a downcasted version of the post if it is from another
model, or itself
"""
if not self.real_type or type(self) != Post:
return self
return getattr(self, self.real_type)
def save(self, make_safe = True, *args, **kwargs):
if type(self) != MenuItem and not self.real_type:
self.real_type = type(self).__name__.lower()
return super().save(*args, **kwargs)

400
cms/sections.py Normal file
View File

@ -0,0 +1,400 @@
import datetime
import re
from enum import Enum, IntEnum
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.utils import timezone as tz
from django.utils.translation import ugettext as _, ugettext_lazy
from wagtail.wagtailcore.models import Page, Orderable
from wagtail.wagtailcore.fields import RichTextField
from wagtail.wagtailimages.edit_handlers import ImageChooserPanel
from wagtail.wagtailadmin.edit_handlers import FieldPanel, FieldRowPanel, \
MultiFieldPanel, InlinePanel, PageChooserPanel, StreamFieldPanel
from wagtail.wagtailsearch import index
# snippets
from wagtail.wagtailsnippets.edit_handlers import SnippetChooserPanel
from wagtail.wagtailsnippets.models import register_snippet
# tags
from modelcluster.models import ClusterableModel
from modelcluster.fields import ParentalKey
from modelcluster.tags import ClusterTaggableManager
from taggit.models import TaggedItemBase
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
#
# Base
#
class BaseRelatedLink(Orderable):
url = models.URLField(
_('url'),
help_text = _('URL of the link'),
)
icon = models.ForeignKey(
'wagtailimages.Image',
verbose_name = _('icon'),
null=True, blank=True,
on_delete=models.SET_NULL,
related_name='+',
help_text = _('icon to display before the url'),
)
title = models.CharField(
_('title'),
max_length = 64,
null = True, blank=True,
help_text = _('text to display of the link'),
)
class Meta:
abstract = True
panels = [
FieldPanel('url'),
FieldRowPanel([
FieldPanel('title'),
ImageChooserPanel('icon'),
]),
]
class BaseList(models.Model):
"""
Generic list
"""
class DateFilter(IntEnum):
none = 0x00
previous = 0x01
next = 0x02
before_related = 0x03,
after_related = 0x04,
filter_date = 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(
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 = {
'publication__isnull': False,
}
)
filter_related = models.ForeignKey(
'Publication',
verbose_name = _('filter by a related publication'),
blank = True, null = True,
help_text = _('if set, select children or siblings of this publication'),
)
related_sibling = models.BooleanField(
verbose_name = _('related is a sibling'),
default = False,
help_text = _('if selected select related publications that are '
'siblings of this one'),
)
sort_asc = models.BooleanField(
verbose_name = _('order ascending'),
default = True,
help_text = _('if selected sort list in the ascending order by date')
)
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):
"""
Get queryset based on the arguments. This class is intended to be
reusable by other classes if needed.
"""
# model
if filter_model:
qs = filter_model.objects.all()
else:
qs = Publication.objects.all()
qs = qs.live().not_in_menu()
# related
if filter_related:
if related_siblings:
qs = qs.sibling_of(filter_related)
else:
qs = qs.descendant_of(filter_related)
date = filter_related.date
if filter_date == cl.DateFilter.before_related:
qs = qs.filter(date__lt = date)
elif filter_date == cl.DateFilter.after_related:
qs = qs.filter(date__gte = date)
# date
else:
date = tz.now()
if filter_date == cl.DateFilter.previous:
qs = qs.filter(date__lt = date)
elif filter_date == cl.DateFilter.next:
qs = qs.filter(date__gte = date)
# sort
if sort_asc:
return qs.order_by('date', 'pk')
return qs.order_by('-date', '-pk')
@classmethod
def get_queryset_from_request(cl, request, *args,
related = 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:
* 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
Context's fields:
* object_list: the final queryset
* list_selector: dict of { 'tag_query', 'search_query' } plus
arguments passed to BaseList.get_base_queryset
* paginator: paginator object
"""
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,
}
qs = cl.get_base_queryset(**kwargs)
# filter by tag
tag = request.GET.get('tag')
if tag:
kwargs['terms'] = tag
qs = qs.filter(tags__name = tag)
# search
search = request.GET.get('search')
if search:
kwargs['terms'] = search
qs = qs.search(search)
context['list_selector'] = kwargs
# 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
class BaseDatedList(models.Model):
"""
List that display items per days. Renders a navigation menu on the
top.
"""
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')
)
class Meta:
abstract = True
panels = [
MultiFieldPanel([
FieldPanel('nav_days'),
FieldPanel('nav_per_week'),
], heading=_('Navigation')),
]
@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<year>[0-9]{4})(-|\/)?(?P<month>[0-9]{1,2})(-|\/)?' \
r'(?P<day>[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_nav_dates(self, date):
"""
Return a list of dates availables for the navigation
"""
if self.nav_per_week:
first = date.weekday()
else:
first = int((self.nav_days - 1) / 2)
first = date - tz.timedelta(days = first)
return [ first + tz.timedelta(days=i)
for i in range(0, self.nav_days) ]
def get_date_context(self, date):
"""
Return a dict that can be added to the context to be used by
a date_list.
"""
today = tz.now().today()
if not date:
date = today
# next/prev weeks/date bunch
dates = self.get_nav_dates(date)
next = date + tz.timedelta(days=self.nav_days)
prev = date - tz.timedelta(days=self.nav_days)
# context dict
return {
'nav_dates': {
'date': date,
'next': next,
'prev': prev,
'dates': dates,
}
}
#
# Sections
#
@register_snippet
class Menu(ClusterableModel):
name = models.CharField(
_('name'),
max_length=32,
blank = True, null = True,
help_text=_('name of this menu (not displayed)'),
)
css_class = models.CharField(
_('CSS class'),
max_length=64,
blank = True, null = True,
help_text=_('menu container\'s "class" attribute')
)
related = models.ForeignKey(
ContentType,
blank = True, null = True,
help_text=_('this menu is displayed only for this model')
)
position = models.CharField(
_('position'),
max_length=16,
blank = True, null = True,
help_text = _('name of the template block in which the menu must '
'be set'),
)
panels = [
MultiFieldPanel([
FieldPanel('name'),
FieldPanel('css_class'),
], heading=_('General')),
MultiFieldPanel([
FieldPanel('related'),
FieldPanel('position'),
], heading=_('Position')),
InlinePanel('menu_items', label=_('menu items')),
]
@register_snippet
class MenuItem(models.Model):
menu = ParentalKey(Menu, related_name='menu_items')
real_type = models.CharField(
max_length=32,
blank = True, null = True,
)
title = models.CharField(
_('title'),
max_length=32,
blank = True, null = True,
)
css_class = models.CharField(
_('CSS class'),
max_length=64,
blank = True, null = True,
help_text=_('menu container\'s "class" attribute')
)
panels = [
MultiFieldPanel([
FieldPanel('name'),
FieldPanel('css_class'),
], heading=_('General')),
]
def specific(self):
"""
Return a downcasted version of the post if it is from another
model, or itself
"""
if not self.real_type or type(self) != Post:
return self
return getattr(self, self.real_type)
def save(self, make_safe = True, *args, **kwargs):
if type(self) != MenuItem and not self.real_type:
self.real_type = type(self).__name__.lower()
return super().save(*args, **kwargs)

View File

@ -8,10 +8,9 @@
{% block title %} {% block title %}
<h1> <h1>
{% if list_selector.search_query %} {% with terms=list_selector.terms %}
{% blocktrans %}Search in publications for <i>{{ search_query }}</i>{% endblocktrans %} {% if terms %}
{% elif list_selector.tag_query %} {% blocktrans %}Search in publications for <i>{{ terms }}</i>{% endblocktrans %}
{% blocktrans %}Search in publications for <i>{{ search_query }}</i>{% endblocktrans %}
{% elif list_selector.filter_related %} {% elif list_selector.filter_related %}
{# should never happen #} {# should never happen #}
{% with title=list_selector.filter_related.title url=list_selector.filter_related.url %} {% with title=list_selector.filter_related.title url=list_selector.filter_related.url %}
@ -21,6 +20,7 @@
{% else %} {% else %}
{% blocktrans %}All the publications{% endblocktrans %} {% blocktrans %}All the publications{% endblocktrans %}
{% endif %} {% endif %}
{% endwith %}
</h1> </h1>
{% endblock %} {% endblock %}

View File

@ -8,6 +8,7 @@
{# list view #} {# list view #}
<div class="body summary"> <div class="body summary">
{{ page.summary }} {{ page.summary }}
<a href="?" class="go_back">{% trans "Go back to the publication" %}</a>
</div> </div>
{% with list_paginator=paginator %} {% with list_paginator=paginator %}