combo/combo/public/templatetags/combo.py

416 lines
13 KiB
Python

# combo - content management system
# Copyright (C) 2014 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import
import datetime
import json
import time
from django import template
from django.conf import settings
from django.core import signing
from django.core.exceptions import PermissionDenied
from django.template import VariableDoesNotExist
from django.template.base import TOKEN_BLOCK, TOKEN_VAR, TOKEN_COMMENT
from django.template import defaultfilters
from django.template.defaultfilters import stringfilter
from django.utils import dateparse, six
from django.utils.encoding import force_text
from combo.data.models import Page, Placeholder
from combo.public.menu import get_menu_context
from combo.utils import NothingInCacheException, flatten_context
from combo.utils.date import make_date, make_datetime
from combo.apps.dashboard.models import DashboardCell, Tile
register = template.Library()
def skeleton_text(context, placeholder_name, content=''):
return '{%% block placeholder-%s %%}{%% block %s %%}%s{%% endblock %%}{%% endblock %%}' % (
placeholder_name, placeholder_name, content)
@register.inclusion_tag('combo/placeholder.html', takes_context=True)
def placeholder(context, placeholder_name, **options):
placeholder = Placeholder(key=placeholder_name, cell=context.get('cell'), **options)
# make sure render_skeleton is available in context
context['render_skeleton'] = context.get('render_skeleton')
if context.get('placeholder_search_mode'):
if placeholder.name:
# only include placeholders with a name
context['placeholders'].append(placeholder)
if not context['traverse_cells']:
return context
context['render'] = True
context['placeholder'] = placeholder
if not placeholder.render:
context['render'] = False
return context
page_cells = []
if 'page_cells' in context:
# page cells are not precomputed when rendering a single cell in an
# ajax call
page_cells = context.get('page_cells')
elif not context.get('render_skeleton'):
page_cells = context['page'].get_cells() if 'page' in context else []
context['cells'] = [x for x in page_cells if
x.placeholder == placeholder_name and
(context.get('render_skeleton') or x.is_relevant(context) and
x.is_visible(context['request'].user))]
if context.get('render_skeleton'):
context['skeleton'] = skeleton_text(context, placeholder_name)
else:
context['skeleton'] = ''
return context
@register.simple_tag(takes_context=True)
def render_cell(context, cell):
if context.get('render_skeleton') and cell.is_user_dependant(context):
context = flatten_context(context)
return template.loader.get_template('combo/deferred-cell.html').render(context)
in_dashboard = False
if DashboardCell.is_enabled():
# check if cell is actually a dashboard tile
try:
tile = Tile.get_by_cell(cell)
except Tile.DoesNotExist:
pass
else:
if context['request'].user != tile.user:
raise PermissionDenied()
in_dashboard = True
context = flatten_context(context)
context['in_dashboard'] = in_dashboard
if 'placeholder' in context and context['placeholder'].force_synchronous:
context['synchronous'] = True
try:
return cell.render(context)
except NothingInCacheException:
return template.loader.get_template('combo/deferred-cell.html').render(context)
except:
if context.get('placeholder_search_mode'):
return ''
raise
@register.tag
def skeleton_extra_placeholder(parser, token):
try:
tag_name, placeholder_name = token.split_contents()
except ValueError:
raise template.TemplateSyntaxError(
"%r tag requires exactly one argument" % token.contents.split()[0]
)
tokens_copy = parser.tokens[:]
text = []
while True:
token = tokens_copy.pop(0)
if token.contents == 'end_skeleton_extra_placeholder':
break
if token.token_type == TOKEN_VAR:
text.append('{{ ')
elif token.token_type == TOKEN_BLOCK:
text.append('{% ')
elif token.token_type == TOKEN_COMMENT:
text.append('{# ')
text.append(token.contents)
if token.token_type == TOKEN_VAR:
text.append(' }}')
elif token.token_type == TOKEN_BLOCK:
text.append(' %}')
elif token.token_type == TOKEN_COMMENT:
text.append(' #}')
nodelist = parser.parse(('end_skeleton_extra_placeholder',))
parser.delete_first_token()
return ExtraPlaceholderNode(nodelist, placeholder_name, ''.join(text))
class ExtraPlaceholderNode(template.Node):
def __init__(self, nodelist, placeholder_name, content):
self.nodelist = nodelist
self.placeholder_name = placeholder_name
self.content = content
def render(self, context):
if not context.get('render_skeleton'):
return self.nodelist.render(context)
return skeleton_text(context, self.placeholder_name, content=self.content)
@register.inclusion_tag('combo/menu.html', takes_context=True)
def show_menu(context, level=0, current_page=None, depth=1, ignore_visibility=True, reduce_depth=False):
if reduce_depth:
depth -= 1
new_context = {
'page': context['page'],
'render_skeleton': context.get('render_skeleton'),
'request': context['request']}
return get_menu_context(new_context, level=level, current_page=current_page,
depth=depth, ignore_visibility=ignore_visibility)
@register.simple_tag(takes_context=True)
def page_absolute_url(context, page):
return context['request'].build_absolute_uri(page.get_online_url())
@register.filter(name='strptime')
@stringfilter
def strptime(date_string, date_format):
try:
return datetime.datetime.strptime(date_string, date_format)
except ValueError:
return None
@register.filter
def parse_date(date_string):
try:
return make_date(date_string)
except ValueError:
pass
# fallback to Django function
try:
return dateparse.parse_date(date_string)
except (ValueError, TypeError):
return None
@register.filter(expects_localtime=True, is_safe=False)
def date(value, arg=None):
if arg is None:
return parse_date(value) or ''
if not isinstance(value, (datetime.datetime, datetime.date, datetime.time)):
value = parse_datetime(value) or parse_date(value)
return defaultfilters.date(value, arg=arg)
@register.filter
def parse_datetime(datetime_string):
try:
return make_datetime(datetime_string)
except ValueError:
pass
# fallback to Django function
try:
return dateparse.parse_datetime(datetime_string)
except (ValueError, TypeError):
return None
@register.filter(name='datetime', expects_localtime=True, is_safe=False)
def datetime_(value, arg=None):
if arg is None:
return parse_datetime(value) or ''
if not isinstance(value, (datetime.datetime, datetime.date, datetime.time)):
value = parse_datetime(value)
return defaultfilters.date(value, arg=arg)
@register.filter
def parse_time(time_string):
# if input is a datetime, extract its time
try:
dt = parse_datetime(time_string)
if dt:
return dt.time()
except (ValueError, TypeError):
pass
# fallback to Django function
try:
return dateparse.parse_time(time_string)
except (ValueError, TypeError):
return None
@register.filter(expects_localtime=True, is_safe=False)
def time(value, arg=None):
if arg is None:
parsed = parse_time(value)
return parsed if parsed is not None else '' # because bool(midnight) == False
if not isinstance(value, (datetime.datetime, datetime.date, datetime.time)):
value = parse_time(value)
return defaultfilters.date(value, arg=arg)
@register.filter
def shown_because_admin(cell, request):
if not (request.user and request.user.is_superuser):
return False
if cell.public:
return False
cell_groups = cell.groups.all()
if not cell_groups:
return False
return not(set(cell_groups).intersection(request.user.groups.all()))
@register.filter(name='has_role')
def has_role(user, groupname):
if not user or user.is_anonymous:
return False
return user.groups.filter(name=groupname).exists()
@register.filter(name='get')
def get(obj, key):
try:
return obj.get(key)
except AttributeError:
return None
@register.filter
def split(string, separator=' '):
return (string or '').split(separator)
@register.filter(name='get_group')
def get_group(group_list, group_name):
ret = []
for group in group_list:
if getattr(group, 'grouper', Ellipsis) == group_name:
# Django >= 1.11, namedtuple
ret.extend(group.list)
elif not hasattr(group, 'grouper') and group['grouper'] == group_name:
ret.extend(group['list'])
return ret
@register.filter(name='is_empty_placeholder')
def is_empty_placeholder(page, placeholder_name):
return len([x for x in page.get_cells() if x.placeholder == placeholder_name]) == 0
@register.filter(name='list')
def as_list(obj):
return list(obj)
@register.filter(name='as_json')
def as_json(obj):
return json.dumps(obj)
@register.filter
def signed(obj):
return signing.dumps(obj)
@register.filter
def name_id(user):
if user and user.is_authenticated:
user_name_id = user.get_name_id()
if user_name_id:
return user_name_id
# it is important to raise this so get_templated_url is aborted and no call
# is tried with a missing user argument.
raise VariableDoesNotExist('name_id')
@register.simple_tag
def get_page(page_slug):
return Page.objects.get(slug=page_slug)
@register.filter
def startswith(string, substring):
return string and force_text(string).startswith(force_text(substring))
def parse_float(value):
if isinstance(value, six.string_types):
# replace , by . for French users comfort
value = value.replace(',', '.')
try:
return float(value)
except (ValueError, TypeError):
return ''
def get_as_datetime(s):
result = parse_datetime(s)
if not result:
result = parse_date(s)
if result:
result = datetime.datetime(year=result.year, month=result.month, day=result.day)
return result
@register.filter(expects_localtime=True, is_safe=False)
def add_days(value, arg):
value = parse_date(value) # consider only date, not hours
if not value:
return ''
arg = parse_float(arg)
if not arg:
return value
result = value + datetime.timedelta(days=float(arg))
return result
@register.filter(expects_localtime=True, is_safe=False)
def add_hours(value, arg):
value = parse_datetime(value)
if not value:
return ''
arg = parse_float(arg)
if not arg:
return value
return value + datetime.timedelta(hours=float(arg))
@register.filter(expects_localtime=True, is_safe=False)
def age_in_days(value, today=None):
value = parse_date(value)
if not value:
return ''
if today is not None:
today = parse_date(today)
if not today:
return ''
else:
today = datetime.date.today()
return (today - value).days
@register.filter(expects_localtime=True, is_safe=False)
def age_in_hours(value, now=None):
# consider value and now as datetimes (and not dates)
value = parse_datetime(value)
if not value:
return ''
if now is not None:
now = parse_datetime(now)
if not now:
return ''
else:
now = datetime.datetime.now()
return int((now - value).total_seconds() / 3600)
def age_in_years_and_months(born, today=None):
'''Compute age since today as the number of years and months elapsed'''
born = make_date(born)
if not born:
return ''
if today is not None:
today = make_date(today)
if not today:
return ''
else:
today = datetime.date.today()
before = (today.month, today.day) < (born.month, born.day)
years = today.year - born.year
months = today.month - born.month
if before:
years -= 1
months += 12
if today.day < born.day:
months -= 1
return years, months
@register.filter(expects_localtime=True, is_safe=False)
def age_in_years(value, today=None):
try:
return age_in_years_and_months(value, today)[0]
except ValueError:
return ''
@register.filter(expects_localtime=True, is_safe=False)
def age_in_months(value, today=None):
try:
years, months = age_in_years_and_months(value, today)
except ValueError:
return ''
return years * 12 + months