Compare commits

..

No commits in common. "main" and "wip/66993-decimal-filter" have entirely different histories.

16 changed files with 114 additions and 1289 deletions

View File

@ -1,2 +0,0 @@
# misc: apply double-quote-string-fixer (#79788)
1684993755341e819948e9a3f81d54bc364bab68

1
.gitignore vendored
View File

@ -1,4 +1,3 @@
**/django.mo
*.pyc
*.egg-info
.pytest_cache/

View File

@ -1,26 +1,18 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: double-quote-string-fixer
- repo: https://github.com/psf/black
rev: 22.3.0
hooks:
- id: black
args: ['--target-version', 'py37', '--skip-string-normalization', '--line-length', '110']
- repo: https://github.com/PyCQA/isort
rev: 5.12.0
rev: 5.7.0
hooks:
- id: isort
args: ['--profile', 'black', '--line-length', '110']
- repo: https://github.com/asottile/pyupgrade
rev: v3.1.0
rev: v2.20.0
hooks:
- id: pyupgrade
args: ['--keep-percent-format', '--py37-plus']
- repo: https://git.entrouvert.org/pre-commit-debian.git
rev: v0.3
hooks:
- id: pre-commit-debian

14
Jenkinsfile vendored
View File

@ -19,18 +19,10 @@ pipeline {
stage('Packaging') {
steps {
script {
env.SHORT_JOB_NAME=sh(
returnStdout: true,
// given JOB_NAME=gitea/project/PR-46, returns project
// given JOB_NAME=project/main, returns project
script: '''
echo "${JOB_NAME}" | sed "s/gitea\\///" | awk -F/ '{print $1}'
'''
).trim()
if (env.GIT_BRANCH == 'main' || env.GIT_BRANCH == 'origin/main') {
sh "sudo -H -u eobuilder /usr/local/bin/eobuilder -d bullseye,bookworm ${SHORT_JOB_NAME}"
if (env.JOB_NAME == 'publik-django-templatetags' && env.GIT_BRANCH == 'origin/main') {
sh 'sudo -H -u eobuilder /usr/local/bin/eobuilder -d buster,bullseye publik-django-templatetags'
} else if (env.GIT_BRANCH.startsWith('hotfix/')) {
sh "sudo -H -u eobuilder /usr/local/bin/eobuilder -d bullseye,bookworm --branch ${env.GIT_BRANCH} --hotfix ${SHORT_JOB_NAME}"
sh "sudo -H -u eobuilder /usr/local/bin/eobuilder -d buster,bullseye --branch ${env.GIT_BRANCH} --hotfix publik-django-templatetags"
}
}
}

View File

@ -2,5 +2,3 @@ include MANIFEST.in
include COPYING
include README
include VERSION
# locales
recursive-include publik_django_templatetags/locale *.po *.mo

9
debian/control vendored
View File

@ -2,17 +2,12 @@ Source: publik-django-templatetags
Maintainer: Entrouvert <info@entrouvert.com>
Section: python
Priority: optional
Build-Depends: debhelper-compat (= 12),
dh-python,
python3-all,
python3-django,
python3-setuptools,
Build-Depends: debhelper-compat (= 12), dh-python, python3-all, python3-setuptools, python3-django
Standards-Version: 3.9.1
Package: python3-publik-django-templatetags
Architecture: all
Depends: ${misc:Depends},
${python3:Depends},
Depends: ${misc:Depends}, ${python3:Depends}
Description: Template tags and filters shared between Publik projects
Django shared application with custom template tags and filters
useful in multiple Publik modules.

View File

@ -1,84 +0,0 @@
# publik-django-templatetags French Translation.
# Copyright (C) 2023 Entr'ouvert
# This file is distributed under the same license as the publik-django-templatetags package.
# Laureline Guerin <lguerin@entrouvert.com>, 2023.
#
msgid ""
msgstr ""
"Project-Id-Version: publik-django-templatetags 0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-08-17 17:48+0200\n"
"PO-Revision-Date: 2023-08-17 17:49+0200\n"
"Last-Translator: Laureline Guerin <lguerin@entrouvert.com>\n"
"Language-Team: French\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: publik/utils.py
#, python-format
msgid "%(first)s and %(second)s"
msgstr "%(first)s et %(second)s"
#: publik/utils.py
msgid ", "
msgstr ", "
#: publik/utils.py
#, python-format
msgid "%(total)s year"
msgid_plural "%(total)s years"
msgstr[0] "%(total)s année"
msgstr[1] "%(total)s années"
#: publik/utils.py
#, python-format
msgid "%(total)s month"
msgid_plural "%(total)s months"
msgstr[0] "%(total)s mois"
msgstr[1] "%(total)s mois"
#: publik/utils.py
#, python-format
msgid "%(total)s day"
msgid_plural "%(total)s days"
msgstr[0] "%(total)s jour"
msgstr[1] "%(total)s jours"
#: publik/utils.py
#, python-format
msgid "%(hours)sh%(minutes)02d"
msgstr "%(hours)sh%(minutes)02d"
#: publik/utils.py
#, python-format
msgid "%(hours)sh"
msgstr "%(hours)sh"
#: publik/utils.py
#, python-format
msgid "%(minutes)smin"
msgstr "%(minutes)smin"
#: publik/utils.py
#, python-format
msgid "%(total)s hour"
msgid_plural "%(total)s hours"
msgstr[0] "%(total)s heure"
msgstr[1] "%(total)s heures"
#: publik/utils.py
#, python-format
msgid "%(total)s minute"
msgid_plural "%(total)s minutes"
msgstr[0] "%(total)s minute"
msgstr[1] "%(total)s minutes"
#: publik/utils.py
#, python-format
msgid "%(total)s second"
msgid_plural "%(total)s seconds"
msgstr[0] "%(total)s seconde"
msgstr[1] "%(total)s secondes"

View File

@ -14,20 +14,14 @@
# 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 collections
import datetime
import math
import re
import urllib.parse
from decimal import Decimal
from decimal import DivisionByZero as DecimalDivisionByZero
from decimal import InvalidOperation as DecimalInvalidOperation
from django import template
from django.template import defaultfilters
from django.utils.encoding import force_str
from publik_django_templatetags.publik import utils
from django.utils.encoding import force_text
register = template.Library()
@ -41,8 +35,6 @@ def get(obj, key):
return obj[key]
except (IndexError, KeyError, TypeError):
return None
except TypeError:
return None
@register.filter
@ -59,40 +51,19 @@ def getlist(mapping, key):
@register.filter(name='list')
def as_list(obj):
try:
return list(obj)
except TypeError:
return []
return list(obj)
@register.filter
def split(string, separator=' '):
return (force_str(string) or '').split(separator)
@register.filter
def removeprefix(string, prefix=None):
if not string:
return ''
value = force_str(string)
prefix = force_str(prefix)
return value.removeprefix(prefix)
@register.filter
def removesuffix(string, suffix=None):
if not string:
return ''
value = force_str(string)
suffix = force_str(suffix)
return value.removesuffix(suffix)
return (force_text(string) or '').split(separator)
@register.filter
def first(value):
try:
return defaultfilters.first(value)
except (TypeError, KeyError):
except TypeError:
return ''
@ -100,19 +71,17 @@ def first(value):
def last(value):
try:
return defaultfilters.last(value)
except (TypeError, KeyError):
except TypeError:
return ''
def parse_decimal(value, default=Decimal(0), do_raise=False):
def parse_decimal(value, default=Decimal(0)):
if isinstance(value, str):
# replace , by . for French users comfort
value = value.replace(',', '.')
try:
return Decimal(value).quantize(Decimal('1.0000')).normalize()
except (ArithmeticError, TypeError):
if do_raise:
raise
return default
@ -129,35 +98,19 @@ def decimal(value, arg=None):
def add(term1, term2):
'''replace the "add" native django filter'''
# consider None content as the empty string
if term1 is None:
term1 = ''
if term2 is None:
term2 = ''
term1_decimal = parse_decimal(term1, default=None)
term2_decimal = parse_decimal(term2, default=None)
# return available number if the other term is the empty string
if term1 == '':
try:
return parse_decimal(term2, do_raise=True)
except (ArithmeticError, TypeError):
pass
if term2 == '':
try:
return parse_decimal(term1, do_raise=True)
except (ArithmeticError, TypeError):
pass
# compute addition if both terms are numbers
try:
return parse_decimal(term1, do_raise=True) + parse_decimal(term2, do_raise=True)
except (ArithmeticError, TypeError, ValueError):
pass
# append to term1 if term1 is a list and not term2
if isinstance(term1, list) and not isinstance(term2, list):
return list(term1) + [term2]
# fallback to django add filter
if term1_decimal is not None and term2_decimal is not None:
return term1_decimal + term2_decimal
if term1 == '' and term2_decimal is not None:
return term2_decimal
if term2 == '' and term1_decimal is not None:
return term1_decimal
return defaultfilters.add(term1, term2)
@ -206,76 +159,3 @@ def sum_(list_):
return sum(parse_decimal(term) for term in list_)
except TypeError: # list_ is not iterable
return ''
@register.filter
def clamp(value, minmax):
try:
value = parse_decimal(value, do_raise=True)
min_value, max_value = (parse_decimal(x, do_raise=True) for x in minmax.split())
except (ArithmeticError, TypeError, ValueError):
return ''
return max(min_value, min(value, max_value))
@register.filter
def limit_low(value, min_value):
try:
return max(parse_decimal(value, do_raise=True), parse_decimal(min_value, do_raise=True))
except (ArithmeticError, TypeError):
return ''
@register.filter
def limit_high(value, max_value):
try:
return min(parse_decimal(value, do_raise=True), parse_decimal(max_value, do_raise=True))
except (ArithmeticError, TypeError):
return ''
@register.filter(is_safe=False)
def duration(value, arg='short'):
if arg not in ('short', 'long'):
return ''
# value is expected to be a timedelta or a number of minutes
if not isinstance(value, datetime.timedelta):
try:
value = datetime.timedelta(seconds=int(value) * 60)
except (TypeError, ValueError):
return ''
return utils.seconds2humanduration(int(value.total_seconds()), short=bool(arg != 'long'))
@register.filter(name='list')
def list_(value):
# turn a generator into a list
if isinstance(value, collections.abc.Iterable) and not isinstance(value, (collections.abc.Mapping, str)):
return list(value)
else:
return [value]
@register.filter
def with_auth(value, arg):
parsed_url = urllib.parse.urlparse(value)
new_netloc = '%s@%s' % (arg, parsed_url.netloc.rsplit('@', 1)[-1])
return urllib.parse.urlunparse(parsed_url._replace(netloc=new_netloc))
@register.filter
def housenumber_number(housenumber):
match = re.match(r'^\s*([0-9]+)(.*)$', force_str(housenumber))
if not match:
return ''
number, btq = match.groups()
return number
@register.filter
def housenumber_btq(housenumber):
match = re.match(r'^\s*([0-9]+)(.*)$', force_str(housenumber))
if not match:
return ''
number, btq = match.groups()
return btq.strip()

View File

@ -1,75 +0,0 @@
# publik-django-templatetags
# Copyright (C) 2023 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 django.utils.translation import gettext_lazy as _
from django.utils.translation import ngettext_lazy
_minute = 60
_hour = 60 * 60
_day = _hour * 24
_month = _day * 31
_year = int(_day * 365.25)
def list2human(stringlist):
'''Transform a string list to human enumeration'''
beginning = stringlist[:-1]
if not beginning:
return ''.join(stringlist)
return _('%(first)s and %(second)s') % {'first': _(', ').join(beginning), 'second': stringlist[-1]}
def seconds2humanduration(seconds, short=False):
"""Convert a time range in seconds to a human string representation"""
if not isinstance(seconds, int):
return ''
if not short:
years = int(seconds / _year)
seconds = seconds - _year * years
months = int(seconds / _month)
seconds = seconds - _month * months
days = int(seconds / _day)
seconds = seconds - _day * days
hours = int(seconds / _hour)
seconds = seconds - _hour * hours
minutes = int(seconds / _minute)
seconds = seconds - _minute * minutes
human = []
if not short:
if years:
human.append(ngettext_lazy('%(total)s year', '%(total)s years', years) % {'total': years})
if months:
human.append(ngettext_lazy('%(total)s month', '%(total)s months', months) % {'total': months})
if days:
human.append(ngettext_lazy('%(total)s day', '%(total)s days', days) % {'total': days})
if short:
if hours and minutes:
human.append(_('%(hours)sh%(minutes)02d') % {'hours': hours, 'minutes': minutes})
elif hours:
human.append(_('%(hours)sh') % {'hours': hours})
elif minutes:
human.append(_('%(minutes)smin') % {'minutes': minutes})
return list2human(human)
else:
if hours:
human.append(ngettext_lazy('%(total)s hour', '%(total)s hours', hours) % {'total': hours})
if minutes:
human.append(ngettext_lazy('%(total)s minute', '%(total)s minutes', minutes) % {'total': minutes})
if seconds:
human.append(ngettext_lazy('%(total)s second', '%(total)s seconds', seconds) % {'total': seconds})
return list2human(human)

View File

@ -14,11 +14,7 @@
# 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 datetime
from django.utils.dateparse import parse_date, parse_datetime
from django.utils.http import urlencode
from django.utils.timezone import is_naive, make_aware
from requests.exceptions import RequestException
from publik_django_templatetags.utils.requests_wrapper import requests
@ -37,16 +33,13 @@ def get_default_wcs_service_key():
class LazyCardDefObjectsManager:
def __init__(
self, service_key, card_id, custom_view_id=None, filters=None, geo_center=None, user=Ellipsis
):
def __init__(self, service_key, card_id, custom_view_id=None, filters=None, user=Ellipsis):
self._service_key = service_key
self._card_id = card_id
self._custom_view_id = custom_view_id
self._filters = filters or {}
self._user = user
self._geo_center = geo_center or {}
self._cached_resultset = None
@ -56,7 +49,6 @@ class LazyCardDefObjectsManager:
card_id=self._card_id,
custom_view_id=self._custom_view_id,
filters=self._filters,
geo_center=self._geo_center,
user=self._user,
)
@ -77,54 +69,6 @@ class LazyCardDefObjectsManager:
qs._filters['full'] = 'on'
return qs
def include_fields(self):
qs = self._clone()
qs._filters['include-fields'] = 'on'
return qs
def include_evolution(self):
qs = self._clone()
qs._filters['include-evolution'] = 'on'
return qs
def include_roles(self):
qs = self._clone()
qs._filters['include-roles'] = 'on'
return qs
def include_submission(self):
qs = self._clone()
qs._filters['include-submission'] = 'on'
return qs
def include_workflow(self):
qs = self._clone()
qs._filters['include-workflow'] = 'on'
return qs
def include_workflow_data(self):
qs = self._clone()
qs._filters['include-workflow-data'] = 'on'
return qs
def get_at(self, value):
qs = self._clone()
if isinstance(value, str):
parsed = parse_datetime(value)
if not parsed:
parsed = parse_date(value)
if parsed:
value = parsed
if isinstance(value, datetime.datetime):
if is_naive(value):
value = make_aware(value)
value = value.isoformat()
elif isinstance(value, datetime.date):
value = make_aware(datetime.datetime.combine(value, datetime.datetime.min.time())).isoformat()
if value:
qs._filters['at'] = value
return qs
def access_control(self, user):
qs = self._clone()
qs._user = user
@ -146,104 +90,31 @@ class LazyCardDefObjectsManager:
value = ''
if isinstance(value, bool):
value = str(value).lower()
op = getattr(self, 'pending_op', 'eq')
if self.pending_attr in ['internal_id', 'number', 'identifier', 'user', 'status', 'distance']:
return getattr(self, 'filter_by_%s' % self.pending_attr)(value, op)
qs._filters['filter-%s' % self.pending_attr] = value
qs._filters['filter-%s-operator' % self.pending_attr] = op
return qs
def apply_op(self, op):
self.pending_op = op
return self
def apply_eq(self):
return self.apply_op('eq')
def apply_ne(self):
return self.apply_op('ne')
def apply_lt(self):
return self.apply_op('lt')
def apply_lte(self):
return self.apply_op('lte')
def apply_gt(self):
return self.apply_op('gt')
def apply_gte(self):
return self.apply_op('gte')
def apply_in(self):
return self.apply_op('in')
def apply_not_in(self):
return self.apply_op('not_in')
def apply_between(self):
return self.apply_op('between')
def apply_absent(self):
self.apply_op('absent')
return self.apply_filter_value('on')
def apply_existing(self):
self.apply_op('existing')
return self.apply_filter_value('on')
def filter_by_internal_id(self, internal_id, op='eq'):
def filter_by_internal_id(self, internal_id):
qs = self._clone()
if internal_id:
qs._filters['filter-internal-id'] = internal_id
qs._filters['filter-internal-id-operator'] = op
return qs
def filter_by_number(self, number, op='eq'):
def filter_by_number(self, number):
qs = self._clone()
if number:
qs._filters['filter-number'] = number
return qs
def filter_by_identifier(self, identifier, op='eq'):
def filter_by_user(self, user):
qs = self._clone()
if identifier:
qs._filters['filter-identifier'] = identifier
if user and user.is_authenticated and user.get_name_id():
qs._filters['filter-user-uuid'] = user.get_name_id()
return qs
def filter_by_user(self, user, op='eq'):
qs = self._clone()
if user:
if hasattr(user, 'is_authenticated') and hasattr(user, 'get_name_id'):
if user.is_authenticated and user.get_name_id():
qs._filters['filter-user-uuid'] = user.get_name_id()
elif isinstance(user, str):
qs._filters['filter-user-uuid'] = user
return qs
def filter_by_status(self, status, op='eq'):
def filter_by_status(self, status):
qs = self._clone()
if status:
qs._filters['filter'] = status
if op in ['eq', 'ne']:
qs._filters['filter-operator'] = op
return qs
def filter_by_distance(self, distance, op=None):
# distance do not currently support an operator
qs = self._clone()
if distance:
qs._filters['filter-distance'] = distance
return qs
def set_geo_center_lat(self, lat):
qs = self._clone()
qs._geo_center['center_lat'] = lat
return qs
def set_geo_center_lon(self, lon):
qs = self._clone()
qs._geo_center['center_lon'] = lon
return qs
def _get_results_from_wcs(self):
@ -257,8 +128,6 @@ class LazyCardDefObjectsManager:
if self._filters:
query = urlencode(self._filters)
api_url += '?%s' % query
if 'center_lat' in self._geo_center and 'center_lon' in self._geo_center:
api_url += '&' + urlencode(self._geo_center)
without_user = self._user is Ellipsis # not set
try:
response = requests.get(

View File

@ -21,268 +21,59 @@ register = template.Library()
@register.filter
def objects(cards, slug):
try:
return getattr(cards, slug).objects
except AttributeError:
return None
return getattr(cards, slug).objects
@register.filter
def with_custom_view(queryset, custom_view_id):
try:
return queryset.with_custom_view(custom_view_id)
except AttributeError:
return None
return queryset.with_custom_view(custom_view_id)
@register.filter
def get_full(queryset):
try:
return queryset.get_full()
except AttributeError:
return None
@register.filter
def include_fields(queryset):
try:
return queryset.include_fields()
except AttributeError:
return None
@register.filter
def include_evolution(queryset):
try:
return queryset.include_evolution()
except AttributeError:
return None
@register.filter
def include_roles(queryset):
try:
return queryset.include_roles()
except AttributeError:
return None
@register.filter
def include_submission(queryset):
try:
return queryset.include_submission()
except AttributeError:
return None
@register.filter
def include_workflow(queryset):
try:
return queryset.include_workflow()
except AttributeError:
return None
@register.filter
def include_workflow_data(queryset):
try:
return queryset.include_workflow_data()
except AttributeError:
return None
@register.filter
def get_at(queryset, value):
return queryset.get_at(value)
return queryset.get_full()
@register.filter
def access_control(queryset, user):
try:
return queryset.access_control(user)
except AttributeError:
return None
return queryset.access_control(user)
@register.filter
def count(queryset):
try:
return queryset.count
except AttributeError:
return 0
return queryset.count
@register.filter
def filter_by(queryset, attribute):
try:
return queryset.filter_by(attribute)
except AttributeError:
return None
return queryset.filter_by(attribute)
@register.filter
def filter_value(queryset, value):
try:
return queryset.apply_filter_value(value)
except AttributeError:
return None
return queryset.apply_filter_value(value)
@register.filter
def filter_by_internal_id(queryset, internal_id):
try:
return queryset.filter_by_internal_id(internal_id)
except AttributeError:
return None
return queryset.filter_by_internal_id(internal_id)
@register.filter
def filter_by_number(queryset, number):
try:
return queryset.filter_by_number(number)
except AttributeError:
return None
@register.filter
def filter_by_identifier(queryset, identifier):
try:
return queryset.filter_by_identifier(identifier)
except AttributeError:
return None
return queryset.filter_by_number(number)
@register.filter
def filter_by_user(queryset, user):
try:
return queryset.filter_by_user(user)
except AttributeError:
return None
return queryset.filter_by_user(user)
@register.filter
def filter_by_status(queryset, status):
try:
return queryset.filter_by_status(status)
except AttributeError:
return None
@register.filter(name='equal')
def eq(queryset):
try:
return queryset.apply_eq()
except AttributeError:
return None
@register.filter(name='not_equal')
def ne(queryset):
try:
return queryset.apply_ne()
except AttributeError:
return None
@register.filter(name='less_than')
def lt(queryset):
try:
return queryset.apply_lt()
except AttributeError:
return None
@register.filter(name='less_than_or_equal')
def lte(queryset):
try:
return queryset.apply_lte()
except AttributeError:
return None
@register.filter(name='greater_than')
def gt(queryset):
try:
return queryset.apply_gt()
except AttributeError:
return None
@register.filter(name='greater_than_or_equal')
def gte(queryset):
try:
return queryset.apply_gte()
except AttributeError:
return None
@register.filter(name='in')
def _in(queryset):
try:
return queryset.apply_in()
except AttributeError:
return None
@register.filter()
def not_in(queryset):
try:
return queryset.apply_not_in()
except AttributeError:
return None
@register.filter()
def between(queryset):
try:
return queryset.apply_between()
except AttributeError:
return None
@register.filter()
def absent(queryset):
try:
return queryset.apply_absent()
except AttributeError:
return None
@register.filter()
def existing(queryset):
try:
return queryset.apply_existing()
except AttributeError:
return None
return queryset.filter_by_status(status)
@register.filter
def order_by(queryset, attribute):
try:
return queryset.order_by(attribute)
except AttributeError:
return None
@register.filter
def filter_by_distance(queryset, distance):
try:
return queryset.filter_by_distance(distance)
except AttributeError:
return None
@register.filter
def set_geo_center_lat(queryset, lat):
try:
return queryset.set_geo_center_lat(lat)
except AttributeError:
return None
@register.filter
def set_geo_center_lon(queryset, lon):
try:
return queryset.set_geo_center_lon(lon)
except AttributeError:
return None
return queryset.order_by(attribute)

View File

@ -40,39 +40,13 @@ def get_version():
real_number, commit_count, commit_hash = result.split('-', 2)
version = '%s.post%s+%s' % (real_number, commit_count, commit_hash)
else:
version = result.replace('.dirty', '+dirty')
version = result
return version
else:
return '0.0.post%s' % len(subprocess.check_output(['git', 'rev-list', 'HEAD']).splitlines())
return '0.0'
class make_translations(Command):
description = 'make message catalogs via django makemessages'
user_options = []
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
curdir = os.getcwd()
try:
from django.core.management import call_command
for path, dirs, files in os.walk('publik_django_templatetags'):
if 'locale' not in dirs:
continue
os.chdir(os.path.realpath(path))
call_command('makemessages', '-l', 'fr', '--add-location', 'file', '--no-obsolete')
except ImportError:
sys.stderr.write('!!! Please install Django >= 2.2 to make translations\n')
finally:
os.chdir(curdir)
class compile_translations(Command):
description = 'compile message catalogs to MO files via django compilemessages'
user_options = []
@ -84,19 +58,18 @@ class compile_translations(Command):
pass
def run(self):
curdir = os.getcwd()
try:
from django.core.management import call_command
for path, dirs, files in os.walk('publik_django_templatetags'):
if 'locale' not in dirs:
continue
curdir = os.getcwd()
os.chdir(os.path.realpath(path))
call_command('compilemessages')
os.chdir(curdir)
except ImportError:
sys.stderr.write('!!! Please install Django >= 2.2 to build translations\n')
finally:
os.chdir(curdir)
class build(_build):
@ -128,17 +101,12 @@ setup(
'Programming Language :: Python',
],
install_requires=[
'django>=3.2, <3.3',
'django',
'requests',
'urllib3<2',
],
setup_requires=[
'django>=2.2, <3.3',
],
zip_safe=False,
cmdclass={
'build': build,
'make_translations': make_translations,
'compile_translations': compile_translations,
'install_lib': install_lib,
'sdist': eo_sdist,

View File

@ -4,7 +4,7 @@ DATABASES = {
'default': {
'ENGINE': os.environ.get('DB_ENGINE', 'django.db.backends.postgresql_psycopg2'),
'NAME': 'publik-django-templatetags-test-%s'
% os.environ.get('BRANCH_NAME', '').replace('/', '-')[:45],
% os.environ.get("BRANCH_NAME", "").replace('/', '-')[:45],
}
}
@ -51,12 +51,12 @@ REQUESTS_TIMEOUT = 25
DEBUG = True
USE_TZ = True
INSTALLED_APPS = [
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sites',
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sites",
]
STATIC_URL = '/static/'
STATIC_URL = "/static/"
SITE_ID = 1
MIDDLEWARE_CLASSES = ()
LOGGING = {}
SECRET_KEY = 'yay'
SECRET_KEY = "yay"

View File

@ -1,5 +1,3 @@
import html
from django.template import Context, Template
@ -65,36 +63,6 @@ def test_split():
assert t.render(Context({'plop': 42})) == '42 '
def test_removeprefix():
t = Template('{{ foo|removeprefix }}')
assert t.render(Context({})) == ''
assert t.render(Context({'foo': None})) == ''
assert t.render(Context({'foo': 'foo bar'})) == 'foo bar'
t = Template('{{ foo|removeprefix:"" }}')
assert t.render(Context({'foo': 'foo bar'})) == 'foo bar'
t = Template('{{ foo|removeprefix:"XY" }}')
assert t.render(Context({'foo': 'XYfoo barXY'})) == 'foo barXY'
assert t.render(Context({'foo': 'foo bar'})) == 'foo bar'
assert t.render(Context({'foo': 'xyfoo barXY'})) == 'xyfoo barXY'
assert t.render(Context({'foo': ' XYfoo barXY'})) == ' XYfoo barXY'
assert t.render(Context({'foo': 'XYXYfoo barXY'})) == 'XYfoo barXY'
def test_removesuffix():
t = Template('{{ foo|removesuffix }}')
assert t.render(Context()) == ''
assert t.render(Context({'foo': None})) == ''
assert t.render(Context({'foo': 'foo bar'})) == 'foo bar'
t = Template('{{ foo|removesuffix:"" }}')
assert t.render(Context({'foo': 'foo bar'})) == 'foo bar'
t = Template('{{ foo|removesuffix:"XY" }}')
assert t.render(Context({'foo': 'XYfoo barXY'})) == 'XYfoo bar'
assert t.render(Context({'foo': 'foo bar'})) == 'foo bar'
assert t.render(Context({'foo': 'XYfoo barxy'})) == 'XYfoo barxy'
assert t.render(Context({'foo': 'XYfoo barXY '})) == 'XYfoo barXY '
assert t.render(Context({'foo': 'XYfoo barXYXY'})) == 'XYfoo barXY'
def test_first():
t = Template('{{ foo|first }}')
@ -110,9 +78,6 @@ def test_first():
context = Context({'foo': None})
assert t.render(context) == ''
context = Context({'foo': {}})
assert t.render(context) == ''
def test_last():
t = Template('{{ foo|last }}')
@ -129,9 +94,6 @@ def test_last():
context = Context({'foo': None})
assert t.render(context) == ''
context = Context({'foo': {}})
assert t.render(context) == ''
def test_decimal():
tmpl = Template('{{ plop|decimal }}')
@ -180,8 +142,8 @@ def test_add():
# using strings
assert tmpl.render(Context({'term1': '1.1', 'term2': 0})) == '1.1'
assert tmpl.render(Context({'term1': 'not a number', 'term2': 1.2})) == ''
assert tmpl.render(Context({'term1': 0.3, 'term2': '1'})) == '1.3'
assert tmpl.render(Context({'term1': 1.4, 'term2': 'not a number'})) == ''
assert tmpl.render(Context({'term1': 0.3, 'term2': "1"})) == '1.3'
assert tmpl.render(Context({'term1': 1.4, 'term2': "not a number"})) == ''
# add
assert tmpl.render(Context({'term1': 4, 'term2': -0.9})) == '3.1'
@ -336,147 +298,3 @@ def test_sum():
assert t.render(Context({'list': None})) == '' # list is not iterable
assert t.render(Context({'list': '123'})) == '' # consider string as not iterable
assert t.render(Context()) == ''
def test_clamp_templatetag():
tmpl = Template('{{ value|clamp:"3.5 5.5" }}')
assert tmpl.render(Context({'value': 4})) == '4'
assert tmpl.render(Context({'value': 6})) == '5.5'
assert tmpl.render(Context({'value': 3})) == '3.5'
assert tmpl.render(Context({'value': 'abc'})) == ''
assert tmpl.render(Context({'value': None})) == ''
tmpl = Template('{{ value|clamp:"3.5 5.5 7.5" }}')
assert tmpl.render(Context({'value': 4})) == ''
tmpl = Template('{{ value|clamp:"a b" }}')
assert tmpl.render(Context({'value': 4})) == ''
def test_limit_templatetags():
for v in (3.5, '"3.5"', 'xxx'):
tmpl = Template('{{ value|limit_low:%s }}' % v)
assert tmpl.render(Context({'value': 4, 'xxx': 3.5})) == '4'
assert tmpl.render(Context({'value': 3, 'xxx': 3.5})) == '3.5'
assert tmpl.render(Context({'value': 'abc', 'xxx': 3.5})) == ''
assert tmpl.render(Context({'value': None, 'xxx': 3.5})) == ''
if v == 'xxx':
assert tmpl.render(Context({'value': 3, 'xxx': 'plop'})) == ''
tmpl = Template('{{ value|limit_high:%s }}' % v)
assert tmpl.render(Context({'value': 4, 'xxx': 3.5})) == '3.5'
assert tmpl.render(Context({'value': 3, 'xxx': 3.5})) == '3'
assert tmpl.render(Context({'value': 'abc', 'xxx': 3.5})) == ''
assert tmpl.render(Context({'value': None, 'xxx': 3.5})) == ''
if v == 'xxx':
assert tmpl.render(Context({'value': 3, 'xxx': 'plop'})) == ''
def test_duration():
context = Context({'value': 2})
assert Template('{{ value|duration }}').render(context) == '2min'
assert Template('{{ value|duration:"long" }}').render(context) == '2 minutes'
context = Context({'value': 80})
assert Template('{{ value|duration }}').render(context) == '1h20'
assert Template('{{ value|duration:"long" }}').render(context) == '1 hour and 20 minutes'
context = Context({'value': 40})
assert Template('{{ value|duration }}').render(context) == '40min'
assert Template('{{ value|duration:"long" }}').render(context) == '40 minutes'
context = Context({'value': 120})
assert Template('{{ value|duration }}').render(context) == '2h'
assert Template('{{ value|duration:"long" }}').render(context) == '2 hours'
context = Context({'value': 1510})
assert Template('{{ value|duration }}').render(context) == '1 day and 1h10'
assert Template('{{ value|duration:"long" }}').render(context) == '1 day, 1 hour and 10 minutes'
context = Context({'value': 61})
assert Template('{{ value|duration }}').render(context) == '1h01'
context = Context({'value': 'xx'})
assert Template('{{ value|duration }}').render(context) == ''
assert Template('{{ value|duration:"long" }}').render(context) == ''
context = Context({'value': 166_660})
assert (
Template('{{ value|duration:"long" }}').render(context)
== '3 months, 22 days, 17 hours and 40 minutes'
)
context = Context({'value': 1_666_666})
assert (
Template('{{ value|duration:"long" }}').render(context)
== '3 years, 1 month, 30 days, 15 hours and 46 minutes'
)
def test_convert_as_list():
tmpl = Template('{{ foo|list|first }}')
assert tmpl.render(Context({'foo': ['foo']})) == 'foo'
def list_generator():
yield from range(5)
assert tmpl.render(Context({'foo': list_generator})) == '0'
def list_range():
return range(5)
assert tmpl.render(Context({'foo': list_range})) == '0'
def test_convert_as_list_with_add():
tmpl = Template('{{ foo|list|add:bar|join:", " }}')
assert tmpl.render(Context({'foo': [1, 2], 'bar': ['a', 'b']})) == '1, 2, a, b'
assert tmpl.render(Context({'foo': [1, 2], 'bar': 'ab'})) == '1, 2, ab'
assert tmpl.render(Context({'foo': 12, 'bar': ['a', 'b']})) == '12, a, b'
assert tmpl.render(Context({'foo': 12, 'bar': 'ab'})) == '12, ab'
assert html.unescape(tmpl.render(Context({'foo': [1, 2], 'bar': {'a': 'b'}}))) == "1, 2, {'a': 'b'}"
assert html.unescape(tmpl.render(Context({'foo': {'a': 'b'}, 'bar': ['a', 'b']}))) == "{'a': 'b'}, a, b"
def test_with_auth():
context = Context({'service_url': 'https://www.example.net/api/whatever?x=y'})
assert (
Template('{{ service_url|with_auth:"username:password" }}').render(context)
== 'https://username:password@www.example.net/api/whatever?x=y'
)
context = Context({'service_url': 'https://a:b@www.example.net/api/whatever?x=y'})
assert (
Template('{{ service_url|with_auth:"username:password" }}').render(context)
== 'https://username:password@www.example.net/api/whatever?x=y'
)
def test_housenumber():
context = Context({'value': '42bis'})
assert Template('{{ value|housenumber_number }}').render(context) == '42'
assert Template('{{ value|housenumber_btq }}').render(context) == 'bis'
context = Context({'value': ' 42 bis '})
assert Template('{{ value|housenumber_number }}').render(context) == '42'
assert Template('{{ value|housenumber_btq }}').render(context) == 'bis'
context = Context({'value': '42 t 3 '})
assert Template('{{ value|housenumber_number }}').render(context) == '42'
assert Template('{{ value|housenumber_btq }}').render(context) == 't 3'
context = Context({'value': ' 42 '})
assert Template('{{ value|housenumber_number }}').render(context) == '42'
assert Template('{{ value|housenumber_btq }}').render(context) == ''
context = Context({'value': ' 42 34 bis '})
assert Template('{{ value|housenumber_number }}').render(context) == '42'
assert Template('{{ value|housenumber_btq }}').render(context) == '34 bis'
context = Context({'value': ' bis '})
assert Template('{{ value|housenumber_number }}').render(context) == ''
assert Template('{{ value|housenumber_btq }}').render(context) == ''
context = Context({'value': 42})
assert Template('{{ value|housenumber_number }}').render(context) == '42'
assert Template('{{ value|housenumber_btq }}').render(context) == ''

View File

@ -1,8 +1,6 @@
import copy
import datetime
import json
from unittest import mock
from urllib.parse import parse_qs, urlparse
import pytest
from django.template import Context, Template
@ -80,7 +78,7 @@ def test_objects(mock_send, settings, context, nocache):
# test filters evaluation
t = Template('{% for card in cards|objects:"foo" %}{{ card.id }} {% endfor %}')
assert t.render(context) == '1 2 '
assert t.render(context) == "1 2 "
assert mock_send.call_args_list[0][0][0].url.startswith(
'http://127.0.0.1:8999/api/cards/foo/list?'
) # primary service
@ -112,17 +110,6 @@ def test_objects(mock_send, settings, context, nocache):
t.render(context)
assert mock_send.call_args_list[0][0][0].url.startswith('http://127.0.0.3:8999/api/cards/bar/list?')
mock_send.reset_mock()
t = Template('{{ cards|get:"bar" }}')
t.render(context)
assert mock_send.call_args_list == []
context = Context({}) # no cards in context
mock_send.reset_mock()
t = Template('{{ cards|objects:"bar" }}')
t.render(context)
assert mock_send.call_args_list == []
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
def test_with_custom_view(mock_send, context, nocache):
@ -147,12 +134,6 @@ def test_with_custom_view(mock_send, context, nocache):
t.render(context)
assert mock_send.call_args_list == [] # unknown, not evaluated
mock_send.reset_mock()
context['foobar'] = None
t = Template('{{ foobar|with_custom_view:"foobar"|list }}')
t.render(context)
assert mock_send.call_args_list == []
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
def test_full(mock_send, context, nocache):
@ -164,86 +145,6 @@ def test_full(mock_send, context, nocache):
t.render(context)
assert 'full=on&' in mock_send.call_args_list[0][0][0].url
mock_send.reset_mock()
context['foobar'] = None
t = Template('{{ foobar|get_full|list }}')
t.render(context)
assert mock_send.call_args_list == []
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
def test_include_filters(mock_send, context, nocache):
for part in ['fields', 'evolution', 'roles', 'submission', 'workflow', 'workflow_data']:
_filter = 'include_%s' % part
param = _filter.replace('_', '-')
t = Template('{{ cards|objects:"foo"|list }}')
t.render(context)
assert '%s=on&' % param not in mock_send.call_args_list[0][0][0].url
mock_send.reset_mock()
t = Template('{{ cards|objects:"foo"|%s|list }}' % _filter)
t.render(context)
assert '%s=on&' % param in mock_send.call_args_list[0][0][0].url
mock_send.reset_mock()
context['foobar'] = None
t = Template('{{ foobar|%s|list }}' % _filter)
t.render(context)
assert mock_send.call_args_list == []
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
def test_at(mock_send, settings, freezer, context, nocache):
settings.TIME_ZONE = 'Europe/Paris'
freezer.move_to('2022-10-17 12:21')
def get_at_param(url):
parsed = parse_qs(urlparse(url).query)
if 'at' not in parsed:
return
return parsed['at'][0]
t = Template('{{ cards|objects:"foo"|list }}')
t.render(context)
assert get_at_param(mock_send.call_args_list[0][0][0].url) is None
mock_send.reset_mock()
t = Template('{{ cards|objects:"foo"|get_at:""|list }}')
t.render(context)
assert get_at_param(mock_send.call_args_list[0][0][0].url) is None
mock_send.reset_mock()
t = Template('{{ cards|objects:"foo"|get_at:none|list }}')
context['none'] = None
t.render(context)
assert get_at_param(mock_send.call_args_list[0][0][0].url) is None
mock_send.reset_mock()
t = Template('{{ cards|objects:"foo"|get_at:"bad-value"|list }}')
t.render(context)
assert get_at_param(mock_send.call_args_list[0][0][0].url) == 'bad-value'
mock_send.reset_mock()
t = Template('{{ cards|objects:"foo"|get_at:"2022-10-17"|list }}')
t.render(context)
assert get_at_param(mock_send.call_args_list[0][0][0].url) == '2022-10-17T00:00:00+02:00'
mock_send.reset_mock()
t = Template('{{ cards|objects:"foo"|get_at:"2022-10-17 23:20"|list }}')
t.render(context)
assert get_at_param(mock_send.call_args_list[0][0][0].url) == '2022-10-17T23:20:00+02:00'
mock_send.reset_mock()
t = Template('{{ cards|objects:"foo"|get_at:a_date|list }}')
context['a_date'] = datetime.date.today()
t.render(context)
assert get_at_param(mock_send.call_args_list[0][0][0].url) == '2022-10-17T00:00:00+02:00'
mock_send.reset_mock()
t = Template('{{ cards|objects:"foo"|get_at:a_datetime|list }}')
context['a_datetime'] = datetime.datetime.now()
t.render(context)
assert get_at_param(mock_send.call_args_list[0][0][0].url) == '2022-10-17T12:21:00+02:00'
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
def test_errors(mock_send, context, nocache):
@ -253,27 +154,27 @@ def test_errors(mock_send, context, nocache):
mock_resp = Response()
mock_resp.status_code = 500
requests_get.return_value = mock_resp
assert t.render(context) == '[]'
assert t.render(context) == "[]"
with mock.patch('publik_django_templatetags.wcs.context_processors.requests.get') as requests_get:
requests_get.side_effect = ConnectionError()
requests_get.return_value = mock_resp
assert t.render(context) == '[]'
assert t.render(context) == "[]"
with mock.patch('publik_django_templatetags.wcs.context_processors.requests.get') as requests_get:
mock_resp = Response()
mock_resp.status_code = 404
requests_get.return_value = mock_resp
assert t.render(context) == '[]'
assert t.render(context) == "[]"
mock_send.side_effect = lambda *a, **k: MockedRequestResponse(content=json.dumps({'err': 1}))
assert t.render(context) == '[]'
assert t.render(context) == "[]"
mock_send.side_effect = lambda *a, **k: MockedRequestResponse(content=json.dumps({}))
assert t.render(context) == '[]'
assert t.render(context) == "[]"
mock_send.side_effect = lambda *a, **k: MockedRequestResponse(content=json.dumps({'data': None}))
assert t.render(context) == '[]'
assert t.render(context) == "[]"
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
@ -328,38 +229,11 @@ def test_access_control(mock_send, context, nocache):
assert 'NameID' not in mock_send.call_args_list[0][0][0].url
assert 'email=foo%40example.net&' in mock_send.call_args_list[0][0][0].url
mock_send.reset_mock()
context['foobar'] = None
t = Template('{{ foobar|access_control:request.user|list }}')
t.render(context)
assert mock_send.call_args_list == []
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
def test_count(mock_send, context, nocache):
t = Template('{{ cards|objects:"foo"|count }}')
assert t.render(context) == '2'
context = Context({'foo': None})
t = Template('{{ foo|count }}')
assert t.render(context) == '0'
OPERATORS = [
('equal', 'eq'),
('not_equal', 'ne'),
('less_than', 'lt'),
('less_than_or_equal', 'lte'),
('greater_than', 'gt'),
('greater_than_or_equal', 'gte'),
('in', 'in'),
('not_in', 'not_in'),
('between', 'between'),
]
OPERATORS_WITHOUT_VALUE = [
('absent', 'absent'),
('existing', 'existing'),
]
assert t.render(context) == "2"
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
@ -392,41 +266,6 @@ def test_filter(mock_send, context, nocache):
t.render(context)
assert 'filter-foo=&' in mock_send.call_args_list[0][0][0].url
context['foobar'] = None
for filter_op, api_op in OPERATORS:
mock_send.reset_mock()
t = Template('{{ cards|objects:"foo"|filter_by:"foo"|%s|filter_value:"bar"|list }}' % filter_op)
t.render(context)
assert 'filter-foo=bar&' in mock_send.call_args_list[0][0][0].url
assert 'filter-foo-operator=%s&' % api_op in mock_send.call_args_list[0][0][0].url
mock_send.reset_mock()
t = Template('{{ foobar|%s }}' % filter_op)
t.render(context)
assert mock_send.call_args_list == []
for filter_op, api_op in OPERATORS_WITHOUT_VALUE:
mock_send.reset_mock()
t = Template('{{ cards|objects:"foo"|filter_by:"foo"|%s|list }}' % filter_op)
t.render(context)
assert 'filter-foo=on&' in mock_send.call_args_list[0][0][0].url
assert 'filter-foo-operator=%s&' % api_op in mock_send.call_args_list[0][0][0].url
mock_send.reset_mock()
t = Template('{{ foobar|%s }}' % filter_op)
t.render(context)
assert mock_send.call_args_list == []
mock_send.reset_mock()
t = Template('{{ foobar|filter_by:"foo"|list }}')
t.render(context)
assert mock_send.call_args_list == []
mock_send.reset_mock()
t = Template('{{ foobar|filter_value:"foo"|list }}')
t.render(context)
assert mock_send.call_args_list == []
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
def test_filter_by_internal_id(mock_send, context, nocache):
@ -434,36 +273,20 @@ def test_filter_by_internal_id(mock_send, context, nocache):
t.render(context)
assert 'filter-internal-id' not in mock_send.call_args_list[0][0][0].url
for tpl in ['filter_by_internal_id', 'filter_by:"internal_id"|filter_value']:
mock_send.reset_mock()
t = Template('{{ cards|objects:"foo"|%s:None|list }}' % tpl)
t.render(context)
assert 'filter-internal-id' not in mock_send.call_args_list[0][0][0].url
mock_send.reset_mock()
t = Template('{{ cards|objects:"foo"|%s:""|list }}' % tpl)
t.render(context)
assert 'filter-internal-id' not in mock_send.call_args_list[0][0][0].url
mock_send.reset_mock()
t = Template('{{ cards|objects:"foo"|%s:"42"|list }}' % tpl)
t.render(context)
assert 'filter-internal-id=42&' in mock_send.call_args_list[0][0][0].url
for filter_op, api_op in OPERATORS:
mock_send.reset_mock()
t = Template(
'{{ cards|objects:"foo"|filter_by:"internal_id"|%s|filter_value:"42"|list }}' % filter_op
)
t.render(context)
assert 'filter-internal-id=42&' in mock_send.call_args_list[0][0][0].url
assert 'filter-internal-id-operator=%s&' % api_op in mock_send.call_args_list[0][0][0].url
mock_send.reset_mock()
t = Template('{{ cards|objects:"foo"|filter_by_internal_id:None|list }}')
t.render(context)
assert 'filter-internal-id' not in mock_send.call_args_list[0][0][0].url
mock_send.reset_mock()
context['foobar'] = None
t = Template('{{ foobar|filter_by_internal_id:"42"|list }}')
t = Template('{{ cards|objects:"foo"|filter_by_internal_id:""|list }}')
t.render(context)
assert mock_send.call_args_list == []
assert 'filter-internal-id' not in mock_send.call_args_list[0][0][0].url
mock_send.reset_mock()
t = Template('{{ cards|objects:"foo"|filter_by_internal_id:"42"|list }}')
t.render(context)
assert 'filter-internal-id=42&' in mock_send.call_args_list[0][0][0].url
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
@ -472,121 +295,42 @@ def test_filter_by_number(mock_send, context, nocache):
t.render(context)
assert 'filter-number' not in mock_send.call_args_list[0][0][0].url
for tpl in ['filter_by_number', 'filter_by:"number"|filter_value']:
mock_send.reset_mock()
t = Template('{{ cards|objects:"foo"|%s:None|list }}' % tpl)
t.render(context)
assert 'filter-number' not in mock_send.call_args_list[0][0][0].url
mock_send.reset_mock()
t = Template('{{ cards|objects:"foo"|%s:""|list }}' % tpl)
t.render(context)
assert 'filter-number' not in mock_send.call_args_list[0][0][0].url
mock_send.reset_mock()
t = Template('{{ cards|objects:"foo"|%s:"42-35"|list }}' % tpl)
t.render(context)
assert 'filter-number=42-35&' in mock_send.call_args_list[0][0][0].url
for filter_op, api_op in OPERATORS:
mock_send.reset_mock()
t = Template('{{ cards|objects:"foo"|filter_by:"number"|%s|filter_value:"42-35"|list }}' % filter_op)
t.render(context)
assert 'filter-number=42-35&' in mock_send.call_args_list[0][0][0].url
# not for this filter
assert 'filter-number-operator=%s&' % api_op not in mock_send.call_args_list[0][0][0].url
mock_send.reset_mock()
t = Template('{{ cards|objects:"foo"|filter_by_number:None|list }}')
t.render(context)
assert 'filter-number' not in mock_send.call_args_list[0][0][0].url
mock_send.reset_mock()
context['foobar'] = None
t = Template('{{ foobar|filter_by_number:"42"|list }}')
t = Template('{{ cards|objects:"foo"|filter_by_number:""|list }}')
t.render(context)
assert mock_send.call_args_list == []
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
def test_filter_by_identifier(mock_send, context, nocache):
t = Template('{{ cards|objects:"foo"|list }}')
t.render(context)
assert 'filter-identifier' not in mock_send.call_args_list[0][0][0].url
for tpl in ['filter_by_identifier', 'filter_by:"identifier"|filter_value']:
mock_send.reset_mock()
t = Template('{{ cards|objects:"foo"|%s:None|list }}' % tpl)
t.render(context)
assert 'filter-identifier' not in mock_send.call_args_list[0][0][0].url
mock_send.reset_mock()
t = Template('{{ cards|objects:"foo"|%s:""|list }}' % tpl)
t.render(context)
assert 'filter-identifier' not in mock_send.call_args_list[0][0][0].url
mock_send.reset_mock()
t = Template('{{ cards|objects:"foo"|%s:"42-35"|list }}' % tpl)
t.render(context)
assert 'filter-identifier=42-35&' in mock_send.call_args_list[0][0][0].url
for filter_op, api_op in OPERATORS:
mock_send.reset_mock()
t = Template(
'{{ cards|objects:"foo"|filter_by:"identifier"|%s|filter_value:"42-35"|list }}' % filter_op
)
t.render(context)
assert 'filter-identifier=42-35&' in mock_send.call_args_list[0][0][0].url
# not for this filter
assert 'filter-identifier-operator=%s&' % api_op not in mock_send.call_args_list[0][0][0].url
assert 'filter-number' not in mock_send.call_args_list[0][0][0].url
mock_send.reset_mock()
context['foobar'] = None
t = Template('{{ foobar|filter_by_identifier:"42"|list }}')
t = Template('{{ cards|objects:"foo"|filter_by_number:"42-35"|list }}')
t.render(context)
assert mock_send.call_args_list == []
assert 'filter-number=42-35&' in mock_send.call_args_list[0][0][0].url
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
def test_filter_by_user(mock_send, context, nocache):
context['nameid'] = 'zyx'
for tpl in ['filter_by_user', 'filter_by:"user"|filter_value']:
t = Template('{{ cards|objects:"foo"|%s:request.user|list }}' % tpl)
mock_send.reset_mock()
context['request'].user = None
t.render(context)
assert 'filter-user-uuid' not in mock_send.call_args_list[0][0][0].url
context['request'].user = MockAnonymousUser()
mock_send.reset_mock()
t.render(context)
assert 'filter-user-uuid' not in mock_send.call_args_list[0][0][0].url
context['request'].user = MockUser()
mock_send.reset_mock()
t.render(context)
assert 'filter-user-uuid' not in mock_send.call_args_list[0][0][0].url
context['request'].user = MockUserWithNameId()
mock_send.reset_mock()
t.render(context)
assert 'filter-user-uuid=xyz&' in mock_send.call_args_list[0][0][0].url
t = Template('{{ cards|objects:"foo"|%s:nameid|list }}' % tpl)
mock_send.reset_mock()
t.render(context)
assert 'filter-user-uuid=zyx&' in mock_send.call_args_list[0][0][0].url
for filter_op, api_op in OPERATORS:
mock_send.reset_mock()
t = Template(
'{{ cards|objects:"foo"|filter_by:"user"|%s|filter_value:request.user|list }}' % filter_op
)
t.render(context)
assert 'filter-user-uuid=xyz&' in mock_send.call_args_list[0][0][0].url
# not for this filter
assert 'filter-user-uuid-operator=%s&' % api_op not in mock_send.call_args_list[0][0][0].url
mock_send.reset_mock()
context['foobar'] = None
t = Template('{{ foobar|filter_by_user:request.user|list }}')
t = Template('{{ cards|objects:"foo"|filter_by_user:request.user|list }}')
t.render(context)
assert mock_send.call_args_list == []
assert 'filter-user-uuid' not in mock_send.call_args_list[0][0][0].url
context['request'].user = MockAnonymousUser()
mock_send.reset_mock()
t.render(context)
assert 'filter-user-uuid' not in mock_send.call_args_list[0][0][0].url
context['request'].user = MockUser()
mock_send.reset_mock()
t.render(context)
assert 'filter-user-uuid' not in mock_send.call_args_list[0][0][0].url
context['request'].user = MockUserWithNameId()
mock_send.reset_mock()
t.render(context)
assert 'filter-user-uuid=xyz&' in mock_send.call_args_list[0][0][0].url
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
@ -594,97 +338,43 @@ def test_filter_by_status(mock_send, context, nocache):
t = Template('{{ cards|objects:"foo"|list }}')
t.render(context)
assert 'filter=&' not in mock_send.call_args_list[0][0][0].url
assert 'filter-operator=eq&' not in mock_send.call_args_list[0][0][0].url
for tpl in ['filter_by_status', 'filter_by:"status"|filter_value']:
mock_send.reset_mock()
t = Template('{{ cards|objects:"foo"|%s:None|list }}' % tpl)
t.render(context)
assert 'filter=&' not in mock_send.call_args_list[0][0][0].url
assert 'filter-operator=eq&' not in mock_send.call_args_list[0][0][0].url
mock_send.reset_mock()
t = Template('{{ cards|objects:"foo"|%s:""|list }}' % tpl)
t.render(context)
assert 'filter=&' not in mock_send.call_args_list[0][0][0].url
assert 'filter-operator=eq&' not in mock_send.call_args_list[0][0][0].url
mock_send.reset_mock()
t = Template('{{ cards|objects:"foo"|%s:"foobar"|list }}' % tpl)
t.render(context)
assert 'filter=foobar&' in mock_send.call_args_list[0][0][0].url
assert 'filter-operator=eq&' in mock_send.call_args_list[0][0][0].url
for filter_op, api_op in OPERATORS:
mock_send.reset_mock()
t = Template('{{ cards|objects:"foo"|filter_by:"status"|%s|filter_value:"foobar"|list }}' % filter_op)
t.render(context)
assert 'filter=foobar&' in mock_send.call_args_list[0][0][0].url
if api_op in ['eq', 'ne']:
assert 'filter-operator=%s&' % api_op in mock_send.call_args_list[0][0][0].url
else:
assert 'filter-operator=%s&' % api_op not in mock_send.call_args_list[0][0][0].url
mock_send.reset_mock()
context['foobar'] = None
t = Template('{{ foobar|filter_by_status:"foobar"|list }}')
t = Template('{{ cards|objects:"foo"|filter_by_status:None|list }}')
t.render(context)
assert mock_send.call_args_list == []
assert 'filter=&' not in mock_send.call_args_list[0][0][0].url
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
def test_filter_by_distance(mock_send, context, nocache):
t = Template('{{ cards|objects:"foo"|list }}')
mock_send.reset_mock()
t = Template('{{ cards|objects:"foo"|filter_by_status:""|list }}')
t.render(context)
assert 'filter-distance=&' not in mock_send.call_args_list[0][0][0].url
assert 'filter=&' not in mock_send.call_args_list[0][0][0].url
for tpl in ['filter_by_distance', 'filter_by:"distance"|filter_value']:
mock_send.reset_mock()
t = Template('{{ cards|objects:"foo"|%s:None|list }}' % tpl)
t.render(context)
assert 'filter-distance=&' not in mock_send.call_args_list[0][0][0].url
mock_send.reset_mock()
t = Template('{{ cards|objects:"foo"|%s:""|list }}' % tpl)
t.render(context)
assert 'filter-distance=&' not in mock_send.call_args_list[0][0][0].url
mock_send.reset_mock()
t = Template('{{ cards|objects:"foo"|%s:"10000"|list }}' % tpl)
t.render(context)
assert 'filter-distance=&' not in mock_send.call_args_list[0][0][0].url
mock_send.reset_mock()
t = Template(
'{{ cards|objects:"foo"|set_geo_center_lat:1|set_geo_center_lon:2|%s:"10000"|list }}' % tpl
)
t.render(context)
assert 'center_lat=1&' in mock_send.call_args_list[0][0][0].url
assert 'center_lon=2&' in mock_send.call_args_list[0][0][0].url
assert 'filter-distance=10000&' in mock_send.call_args_list[0][0][0].url
assert 'filter-distance-operator' not in mock_send.call_args_list[0][0][0].url
mock_send.reset_mock()
t = Template('{{ cards|objects:"foo"|filter_by_status:"foobar"|list }}')
t.render(context)
assert 'filter=foobar&' in mock_send.call_args_list[0][0][0].url
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
def test_getlist(mock_send, context, nocache):
t = Template('{% for v in cards|objects:"foo"|getlist:"id" %}{{ v }},{% endfor %}')
t.render(context)
assert t.render(context) == '1,2,'
assert t.render(context) == "1,2,"
t = Template('{% for v in cards|objects:"foo"|getlist:"fields" %}{{ v }},{% endfor %}')
t.render(context)
result = t.render(context)
if '&#39;' in result:
# django 2.2
assert result == '{&#39;foo&#39;: &#39;bar&#39;},{&#39;foo&#39;: &#39;baz&#39;},'
assert result == "{&#39;foo&#39;: &#39;bar&#39;},{&#39;foo&#39;: &#39;baz&#39;},"
else:
# django 3.2
assert result == '{&#x27;foo&#x27;: &#x27;bar&#x27;},{&#x27;foo&#x27;: &#x27;baz&#x27;},'
assert result == "{&#x27;foo&#x27;: &#x27;bar&#x27;},{&#x27;foo&#x27;: &#x27;baz&#x27;},"
t = Template('{% for v in cards|objects:"foo"|getlist:"fields"|getlist:"foo" %}{{ v }},{% endfor %}')
t.render(context)
assert t.render(context) == 'bar,baz,'
assert t.render(context) == "bar,baz,"
t = Template('{% for v in cards|objects:"foo"|getlist:"fields"|getlist:"unknown" %}{{ v }},{% endfor %}')
t.render(context)
assert t.render(context) == 'None,None,'
assert t.render(context) == "None,None,"
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
@ -717,9 +407,3 @@ def test_order_by(mock_send, context, nocache):
t = Template('{% for v in cards|objects:"foo"|order_by:""|order_by:"bar" %}{{ v }},{% endfor %}')
t.render(context)
assert 'order_by=bar' in mock_send.call_args_list[0][0][0].url
mock_send.reset_mock()
context['foobar'] = None
t = Template('{{ foobar|order_by:"foo" }}')
t.render(context)
assert mock_send.call_args_list == []

View File

@ -1,5 +1,5 @@
[tox]
envlist = py3-django32-codestyle-coverage
envlist = py3-django{22,32}-codestyle-coverage
toxworkdir = {env:TMPDIR:/tmp}/tox-{env:USER}/publik-django-templatetags/{env:BRANCH_NAME:}
[testenv]
@ -11,14 +11,14 @@ setenv =
JUNIT=--junitxml=junit-{envname}.xml
coverage: COVERAGE=--cov-report xml --cov-report html --cov=publik_django_templatetags/
deps =
django22: django>=2.2,<2.3
django32: django>=3.2,<3.3
pytest
pytest-cov
pytest-django
pytest-freezegun
WebTest
psycopg2-binary
psycopg2
psycopg2-binary<2.9
psycopg2<2.9
pre-commit
commands =
pytest {posargs: {env:JUNIT:} {env:COVERAGE:} tests/}