combo/combo/public/templatetags/combo.py

614 lines
19 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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/>.
import copy
import datetime
import json
import django
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
try:
from django.template.base import TokenType
TOKEN_BLOCK = TokenType.BLOCK
TOKEN_VAR = TokenType.VAR
TOKEN_COMMENT = TokenType.COMMENT
except ImportError:
from django.template.base import TOKEN_BLOCK, TOKEN_VAR, TOKEN_COMMENT
from django.template import Template, TemplateSyntaxError, defaultfilters
from django.template.defaultfilters import stringfilter
from django.utils import dateparse
from django.utils.encoding import force_str
from django.utils.timezone import is_naive, make_aware
from requests.exceptions import RequestException
from combo.apps.dashboard.models import DashboardCell, Tile
from combo.data.models import Page, Placeholder, element_is_visible
from combo.public.menu import get_menu_context
from combo.utils import NothingInCacheException, flatten_context, requests
from combo.utils.date import make_date, make_datetime
from combo.utils.requests_wrapper import WaitForCacheException
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)
if 'page' in context:
context['placeholder_options'] = context['page'].placeholder_options.get(placeholder_name) or {}
# 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 'page' in context and hasattr(context['page'], 'prefetched_cells'):
# sometimes cells are prefetched; use them
page_cells = context['page'].prefetched_cells
elif not context.get('render_skeleton'):
page_cells = context['page'].get_cells() if 'page' in context else []
if 'page' in context:
# store cells for later use
context['page'].prefetched_cells = page_cells
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'], check_validity_info=False)
)
]
if context.get('render_skeleton'):
context['skeleton'] = skeleton_text(context, placeholder_name)
else:
context['skeleton'] = ''
if not context.get('placeholder_search_mode'):
if len(context['cells']) and placeholder.outer_tag:
context['outer_tag'] = placeholder.outer_tag
for cell in [x for x in context['cells'] if hasattr(x, 'get_repeat_template')]:
repeat_template = cell.get_repeat_template(context)
if not repeat_template:
continue
try:
repeat = int(Template(repeat_template).render(context))
except (ValueError, TemplateSyntaxError, VariableDoesNotExist):
continue
cell_idx = context['cells'].index(cell)
context['cells'].remove(cell)
if repeat == 0:
continue
repeated_cells = []
for i in range(repeat):
new_cell = copy.copy(cell)
new_cell.repeat_index = i
new_cell.include_pagination = bool(i + 1 == repeat)
if hasattr(new_cell, 'set_data_from_repeated_cell'):
new_cell.set_data_from_repeated_cell(cell, context)
repeated_cells.append(new_cell)
context['cells'][cell_idx:cell_idx] = repeated_cells
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 WaitForCacheException:
return 'retry'
except Exception:
if context.get('placeholder_search_mode'):
return ''
raise
@register.simple_tag(takes_context=True)
def render_filters(context, cell):
return cell.render_filters(context)
@register.tag
def skeleton_extra_placeholder(parser, token):
try:
dummy, placeholder_name = token.split_contents()
except ValueError:
raise template.TemplateSyntaxError('%r tag requires exactly one argument' % token.contents.split()[0])
tokens_copy = parser.tokens[:]
if django.VERSION < (3,):
tokens_copy.reverse()
text = []
while True:
token = tokens_copy.pop()
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, is_submenu=False
):
if reduce_depth:
depth -= 1
new_context = {
'page': context['page'],
'render_skeleton': context.get('render_skeleton'),
'request': context['request'],
'is_submenu': is_submenu,
}
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):
return not element_is_visible(cell, user=request.user, ignore_superuser=True)
@register.filter(name='has_role')
def has_role(user, groupname):
if not user or getattr(user, 'is_anonymous', True):
return False
if not hasattr(user, 'groups'):
return False
return user.groups.filter(name=groupname).exists()
@register.filter
def strip(string, chars=None):
if not string:
return ''
if chars:
return force_str(string).strip(force_str(chars))
else:
return force_str(string).strip()
@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='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.filter
def user_id_for_service(user, service_slug):
name_id = None
user_id = ''
if isinstance(user, str):
name_id = user
elif user and getattr(user, 'is_authenticated', False) and callable(getattr(user, 'get_name_id', None)):
name_id = user.get_name_id()
if not name_id:
return ''
for authentic in settings.KNOWN_SERVICES.get('authentic', {}).values():
api = 'api/users/%s/service/%s/' % (name_id, service_slug)
try:
response = requests.get(
api, remote_service=authentic, without_user=True, timeout=5, log_errors=False
)
response.raise_for_status()
except RequestException:
continue
try:
result = response.json()
user_id = result['data']['user']['id']
except (json.JSONDecodeError, KeyError, TypeError, ValueError):
continue
if user_id:
break
return user_id if user_id is not None else ''
@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_str(string).startswith(force_str(substring))
@register.filter
def endswith(string, substring):
return string and force_str(string).endswith(force_str(substring))
def parse_float(value):
if isinstance(value, str):
# 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
@register.filter(expects_localtime=True)
def datetime_in_past(value):
value = parse_datetime(value)
if not value:
return False
if is_naive(value):
value = make_aware(value)
date_now = make_aware(datetime.datetime.now())
return value <= date_now
@register.filter(expects_localtime=True)
def adjust_to_week_monday(value):
value = parse_date(value)
if not value:
return ''
return value - datetime.timedelta(days=value.weekday())
@register.filter(expects_localtime=True)
def iterate_days_until(value, until):
value = parse_date(value)
until = parse_date(until)
if not (value and until):
return
while value < until:
yield value
value = value + datetime.timedelta(days=1)
yield value
@register.filter(is_safe=False)
def phonenumber_fr(value, separator=' '):
DROMS = ('262', '508', '590', '594', '596')
if not value or not isinstance(value, str):
return value
number = value.strip()
if not number:
return value
if number[0] == '+':
international = '+'
number = '00' + number[1:]
else:
international = '00' + separator
number = ''.join(c for c in number if c in '0123456789')
def in_pairs(num):
return separator.join(num[i * 2 : i * 2 + 2] for i in range(len(num) // 2))
# local number
if len(number) == 10 and number[0] == '0' and number[1] in '123456789':
return in_pairs(number)
# international
if len(number) == 14 and number[0:5] == '00330':
# +/00 33 (0)x xx xx xx xx : remove (0)
number = number[0:4] + number[5:]
if len(number) == 13 and number[0:4] == '0033':
return international + '33' + separator + number[4] + separator + in_pairs(number[5:])
if len(number) == 11 and number[0:2] == '00' and number[2:5] in DROMS:
return international + number[2:5] + separator + in_pairs(number[5:])
# unknown
return value
@register.filter
def extra_context(cell, request):
ctx = copy.copy(getattr(request, 'extra_context_data', {}) or {})
if hasattr(cell, 'repeat_index'):
ctx['repeat_index'] = cell.repeat_index
return ctx