Compare commits

...

41 Commits

Author SHA1 Message Date
Thomas NOËL 83f99fafee publik: add housenumber number/btq filters (#88884)
gitea/publik-django-templatetags/pipeline/head This commit looks good Details
2024-04-04 13:52:26 +02:00
Lauréline Guérin e68e6e7ff9
publik: fix get filter called on LazyCardDef (#88578)
gitea/publik-django-templatetags/pipeline/head This commit looks good Details
2024-04-03 15:42:14 +02:00
Frédéric Péters 6ad1cc0911 wcs: add support for |filter_by_identifier (#85618)
gitea/publik-django-templatetags/pipeline/head This commit looks good Details
2024-02-18 12:32:25 +01:00
Benjamin Dauvergne 7bb34ba534 misc: include locale/ in source dist (#86922)
gitea/publik-django-templatetags/pipeline/head This commit looks good Details
2024-02-13 16:45:47 +01:00
Emmanuel Cazenave e912e4c687 setup: compute pep440 compliant dirty version number (#81731)
gitea/publik-django-templatetags/pipeline/head This commit looks good Details
2023-10-30 17:37:58 +01:00
Frédéric Péters e1e6d17d2f misc: add |with_auth filter tag, to add basic HTTP auth to URL (#80394)
gitea/publik-django-templatetags/pipeline/head This commit looks good Details
2023-10-03 14:13:35 +02:00
Lauréline Guérin 063f03c491
publik: use list/add filters to create list from simple values (#81223)
gitea/publik-django-templatetags/pipeline/head This commit looks good Details
2023-09-19 10:55:26 +02:00
Lauréline Guérin 6e9d0a8b63
publik: add |clamp, |limit_low and |limit_high filters (#81223) 2023-09-19 10:46:21 +02:00
Frédéric Péters b32771096e misc: restore |duration unit as minutes (#80435)
gitea/publik-django-templatetags/pipeline/head This commit looks good Details
2023-08-18 09:54:00 +02:00
Frédéric Péters a77c14e614 translation update
gitea/publik-django-templatetags/pipeline/head This commit looks good Details
2023-08-17 17:49:03 +02:00
Frédéric Péters bbc215bfb6 misc: add months and years to duration (#80421)
gitea/publik-django-templatetags/pipeline/head This commit looks good Details
2023-08-17 13:48:21 +02:00
Frédéric Péters 1b36a580ea misc: fix |duration to take seconds as parameter (#80421) 2023-08-17 13:48:21 +02:00
Valentin Deniaud 542b25d7e0 misc: update git-blame-ignore-revs to ignore quote changes (#79788)
gitea/publik-django-templatetags/pipeline/head This commit looks good Details
2023-08-16 10:42:35 +02:00
Valentin Deniaud 1684993755 misc: apply double-quote-string-fixer (#79788) 2023-08-16 10:31:35 +02:00
Valentin Deniaud c3534c25bf misc: add pre commit hook to force single quotes (#79788) 2023-08-16 10:31:35 +02:00
Lauréline Guérin d232a663e1
wcs: add new operators (#79828)
gitea/publik-django-templatetags/pipeline/head This commit looks good Details
2023-07-20 09:31:39 +02:00
Frédéric Péters 9d4cf4f53b ci: build deb package for bookworm (#78971)
gitea/publik-django-templatetags/pipeline/head This commit looks good Details
2023-06-23 14:39:06 +02:00
Frédéric Péters ea5ec534d5 debian: apply new pre-commit-debian (#77727)
gitea/publik-django-templatetags/pipeline/head This commit looks good Details
2023-05-28 21:23:02 +02:00
Frédéric Péters d9796d27c7 misc: add pre-commit-debian (#77727) 2023-05-28 21:22:53 +02:00
Frédéric Péters efa1342a42 build: allow django 2.2 to build package (#77647)
gitea/publik-django-templatetags/pipeline/head This commit looks good Details
2023-05-16 10:55:06 +02:00
Frédéric Péters b458b1ceb1 build: chdir back to working directory after makemessages error (#77628)
gitea/publik-django-templatetags/pipeline/head There was a failure building this commit Details
2023-05-15 17:02:11 +02:00
Frédéric Péters a9d0b2f3f0 build: add specific version requirements (#77603)
gitea/publik-django-templatetags/pipeline/head There was a failure building this commit Details
2023-05-15 15:04:26 +02:00
Frédéric Péters 4bfda4f828 build: add django to setup_requires (#77586)
gitea/publik-django-templatetags/pipeline/head This commit looks good Details
2023-05-15 11:33:31 +02:00
Lauréline Guérin bdc8a59ff0
publik: add removeprefix and removesuffix filters (#74786)
gitea/publik-django-templatetags/pipeline/head This commit looks good Details
2023-04-28 11:27:38 +02:00
Lauréline Guérin c1e3cba42f
wcs: filter_by_user with nameid as value (#76604)
gitea/publik-django-templatetags/pipeline/head This commit looks good Details
2023-04-13 14:51:54 +02:00
Lauréline Guérin b41e397d02
misc: remove tests on django 2.2 (#76604) 2023-04-13 14:48:40 +02:00
Lauréline Guérin efae69e50d
translation update
gitea/publik-django-templatetags/pipeline/head This commit looks good Details
2023-02-27 21:45:32 +01:00
Lauréline Guérin 4141f48cd7
misc: add make_translation command (#72667) 2023-02-27 21:45:32 +01:00
Lauréline Guérin 9bc4981677
publik: add duration filter (#72667) 2023-02-27 21:45:32 +01:00
Agate 1c7d91bb19 Prepare Jenkinsfile for Gitea migration (#74572)
gitea/publik-django-templatetags/pipeline/head This commit looks good Details
2023-02-20 15:16:13 +01:00
Frédéric Péters 1abf7c5d76 ci: upgrade isort (#74044) 2023-02-01 09:29:08 +01:00
Lauréline Guérin 04f16f64ee
wcs: add |get_at filter (#70345) 2022-12-28 15:13:30 +01:00
Frédéric Péters ee9e74ce80 ci: only build package for bullseye (#72729) 2022-12-22 17:21:29 +01:00
Frédéric Péters 4b16a94262 wcs: add distance filters (#70588) 2022-12-01 18:08:54 +01:00
Lauréline Guérin 8f6fd7b2fe
wcs: fix |objects filter without cards in context (#67554) 2022-11-17 09:08:23 +01:00
Lauréline Guérin f0a435dd8a
wcs: fix templatefilters on AttributeError (#71382) 2022-11-16 19:47:14 +01:00
Lauréline Guérin 77b277fc63
wcs: add filters to get only fields, evolution, etc (#71299)
added filters:
- |include_fields
- |include_evolutions
- |include_roles
- |include_submission
- |include_workflow
- |include_workflow_data
2022-11-15 15:53:42 +01:00
Lauréline Guérin c5b89d13a2
wcs: fix |count query None queryset (#70955) 2022-11-03 14:11:06 +01:00
Lauréline Guérin a49ab97480
publik: fix |first & |last on KeyError (#70954) 2022-11-03 10:00:18 +01:00
Frédéric Péters df0a449167 ci: update pyupgrade to 3.1.0 (#70693) 2022-10-26 19:22:45 +02:00
Lauréline Guérin 270e0b51dc
wcs: add operators (#70125) 2022-10-13 15:56:38 +02:00
16 changed files with 1285 additions and 110 deletions

2
.git-blame-ignore-revs Normal file
View File

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

1
.gitignore vendored
View File

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

View File

@ -1,18 +1,26 @@
# 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.7.0
rev: 5.12.0
hooks:
- id: isort
args: ['--profile', 'black', '--line-length', '110']
- repo: https://github.com/asottile/pyupgrade
rev: v2.20.0
rev: v3.1.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,10 +19,18 @@ pipeline {
stage('Packaging') {
steps {
script {
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'
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}"
} else if (env.GIT_BRANCH.startsWith('hotfix/')) {
sh "sudo -H -u eobuilder /usr/local/bin/eobuilder -d buster,bullseye --branch ${env.GIT_BRANCH} --hotfix publik-django-templatetags"
sh "sudo -H -u eobuilder /usr/local/bin/eobuilder -d bullseye,bookworm --branch ${env.GIT_BRANCH} --hotfix ${SHORT_JOB_NAME}"
}
}
}

View File

@ -2,3 +2,5 @@ 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,12 +2,17 @@ Source: publik-django-templatetags
Maintainer: Entrouvert <info@entrouvert.com>
Section: python
Priority: optional
Build-Depends: debhelper-compat (= 12), dh-python, python3-all, python3-setuptools, python3-django
Build-Depends: debhelper-compat (= 12),
dh-python,
python3-all,
python3-django,
python3-setuptools,
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

@ -0,0 +1,84 @@
# 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,14 +14,20 @@
# 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_text
from django.utils.encoding import force_str
from publik_django_templatetags.publik import utils
register = template.Library()
@ -35,6 +41,8 @@ def get(obj, key):
return obj[key]
except (IndexError, KeyError, TypeError):
return None
except TypeError:
return None
@register.filter
@ -51,19 +59,40 @@ def getlist(mapping, key):
@register.filter(name='list')
def as_list(obj):
return list(obj)
try:
return list(obj)
except TypeError:
return []
@register.filter
def split(string, separator=' '):
return (force_text(string) or '').split(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)
@register.filter
def first(value):
try:
return defaultfilters.first(value)
except TypeError:
except (TypeError, KeyError):
return ''
@ -71,17 +100,19 @@ def first(value):
def last(value):
try:
return defaultfilters.last(value)
except TypeError:
except (TypeError, KeyError):
return ''
def parse_decimal(value, default=Decimal(0)):
def parse_decimal(value, default=Decimal(0), do_raise=False):
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
@ -98,19 +129,35 @@ 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)
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 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
return defaultfilters.add(term1, term2)
@ -159,3 +206,76 @@ 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

@ -0,0 +1,75 @@
# 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,7 +14,11 @@
# 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
@ -33,13 +37,16 @@ def get_default_wcs_service_key():
class LazyCardDefObjectsManager:
def __init__(self, service_key, card_id, custom_view_id=None, filters=None, user=Ellipsis):
def __init__(
self, service_key, card_id, custom_view_id=None, filters=None, geo_center=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
@ -49,6 +56,7 @@ 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,
)
@ -69,6 +77,54 @@ 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
@ -90,31 +146,104 @@ 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 filter_by_internal_id(self, internal_id):
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'):
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):
def filter_by_number(self, number, op='eq'):
qs = self._clone()
if number:
qs._filters['filter-number'] = number
return qs
def filter_by_user(self, user):
def filter_by_identifier(self, identifier, op='eq'):
qs = self._clone()
if user and user.is_authenticated and user.get_name_id():
qs._filters['filter-user-uuid'] = user.get_name_id()
if identifier:
qs._filters['filter-identifier'] = identifier
return qs
def filter_by_status(self, status):
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'):
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):
@ -128,6 +257,8 @@ 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,59 +21,268 @@ register = template.Library()
@register.filter
def objects(cards, slug):
return getattr(cards, slug).objects
try:
return getattr(cards, slug).objects
except AttributeError:
return None
@register.filter
def with_custom_view(queryset, custom_view_id):
return queryset.with_custom_view(custom_view_id)
try:
return queryset.with_custom_view(custom_view_id)
except AttributeError:
return None
@register.filter
def get_full(queryset):
return queryset.get_full()
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)
@register.filter
def access_control(queryset, user):
return queryset.access_control(user)
try:
return queryset.access_control(user)
except AttributeError:
return None
@register.filter
def count(queryset):
return queryset.count
try:
return queryset.count
except AttributeError:
return 0
@register.filter
def filter_by(queryset, attribute):
return queryset.filter_by(attribute)
try:
return queryset.filter_by(attribute)
except AttributeError:
return None
@register.filter
def filter_value(queryset, value):
return queryset.apply_filter_value(value)
try:
return queryset.apply_filter_value(value)
except AttributeError:
return None
@register.filter
def filter_by_internal_id(queryset, internal_id):
return queryset.filter_by_internal_id(internal_id)
try:
return queryset.filter_by_internal_id(internal_id)
except AttributeError:
return None
@register.filter
def filter_by_number(queryset, number):
return queryset.filter_by_number(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
@register.filter
def filter_by_user(queryset, user):
return queryset.filter_by_user(user)
try:
return queryset.filter_by_user(user)
except AttributeError:
return None
@register.filter
def filter_by_status(queryset, status):
return queryset.filter_by_status(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
@register.filter
def order_by(queryset, attribute):
return queryset.order_by(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

View File

@ -40,13 +40,39 @@ 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
version = result.replace('.dirty', '+dirty')
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 = []
@ -58,18 +84,19 @@ 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):
@ -101,12 +128,17 @@ setup(
'Programming Language :: Python',
],
install_requires=[
'django',
'django>=3.2, <3.3',
'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,3 +1,5 @@
import html
from django.template import Context, Template
@ -63,6 +65,36 @@ 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 }}')
@ -78,6 +110,9 @@ 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 }}')
@ -94,6 +129,9 @@ 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 }}')
@ -142,8 +180,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'
@ -298,3 +336,147 @@ 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,6 +1,8 @@
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
@ -78,7 +80,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
@ -110,6 +112,17 @@ 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):
@ -134,6 +147,12 @@ 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):
@ -145,6 +164,86 @@ 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):
@ -154,27 +253,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)
@ -229,11 +328,38 @@ 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"
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'),
]
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
@ -266,6 +392,41 @@ 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):
@ -273,20 +434,36 @@ 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
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
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:""|list }}')
context['foobar'] = None
t = Template('{{ foobar|filter_by_internal_id:"42"|list }}')
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"|filter_by_internal_id:"42"|list }}')
t.render(context)
assert 'filter-internal-id=42&' in mock_send.call_args_list[0][0][0].url
assert mock_send.call_args_list == []
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
@ -295,42 +472,121 @@ 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
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
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:""|list }}')
context['foobar'] = None
t = Template('{{ foobar|filter_by_number:"42"|list }}')
t.render(context)
assert 'filter-number' not in mock_send.call_args_list[0][0][0].url
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
mock_send.reset_mock()
t = Template('{{ cards|objects:"foo"|filter_by_number:"42-35"|list }}')
context['foobar'] = None
t = Template('{{ foobar|filter_by_identifier:"42"|list }}')
t.render(context)
assert 'filter-number=42-35&' in mock_send.call_args_list[0][0][0].url
assert mock_send.call_args_list == []
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
def test_filter_by_user(mock_send, context, nocache):
t = Template('{{ cards|objects:"foo"|filter_by_user:request.user|list }}')
t.render(context)
assert 'filter-user-uuid' not in mock_send.call_args_list[0][0][0].url
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 = 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 = 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
context['request'].user = MockUserWithNameId()
mock_send.reset_mock()
context['foobar'] = None
t = Template('{{ foobar|filter_by_user:request.user|list }}')
t.render(context)
assert 'filter-user-uuid=xyz&' in mock_send.call_args_list[0][0][0].url
assert mock_send.call_args_list == []
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
@ -338,43 +594,97 @@ 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()
t = Template('{{ cards|objects:"foo"|filter_by_status:None|list }}')
context['foobar'] = None
t = Template('{{ foobar|filter_by_status:"foobar"|list }}')
t.render(context)
assert 'filter=&' not in mock_send.call_args_list[0][0][0].url
assert mock_send.call_args_list == []
mock_send.reset_mock()
t = Template('{{ cards|objects:"foo"|filter_by_status:""|list }}')
t.render(context)
assert 'filter=&' 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 }}')
@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 }}')
t.render(context)
assert 'filter=foobar&' in mock_send.call_args_list[0][0][0].url
assert 'filter-distance=&' 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.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)
@ -407,3 +717,9 @@ 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-django{22,32}-codestyle-coverage
envlist = py3-django32-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<2.9
psycopg2<2.9
psycopg2-binary
psycopg2
pre-commit
commands =
pytest {posargs: {env:JUNIT:} {env:COVERAGE:} tests/}