From 2f030fd345da5b531cf0af9489cf3c1b9b5e1adf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= Date: Fri, 19 Jun 2020 14:48:31 +0200 Subject: [PATCH] templates: add filters for working days (#31851) --- debian/control | 1 + tests/test_templates.py | 252 +++++++++++++++++++++++++++++- tox.ini | 1 + wcs/qommon/calendar.py | 41 +++++ wcs/qommon/publisher.py | 3 + wcs/qommon/templatetags/qommon.py | 61 ++++++++ wcs/settings.py | 3 + 7 files changed, 357 insertions(+), 5 deletions(-) create mode 100644 wcs/qommon/calendar.py diff --git a/debian/control b/debian/control index 82acea38e..f70fa3488 100644 --- a/debian/control +++ b/debian/control @@ -33,6 +33,7 @@ Recommends: libreoffice-writer-nogui | libreoffice-writer, python3-langdetect, python3-magic, python3-qrcode, + python3-workalendar, python3-xlwt Suggests: python3-libxml2 Description: web application to design and set up online forms diff --git a/tests/test_templates.py b/tests/test_templates.py index 258b92589..db5e2e09e 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import datetime +import os import pytest import string @@ -11,7 +12,8 @@ except ImportError: from django.test import override_settings from django.utils.timezone import now -from quixote import cleanup + +from wcs.qommon.http_request import HTTPRequest from wcs.qommon.substitution import CompatibilityNamesDict from wcs.qommon.template import Template, TemplateError from wcs.variables import LazyFormData @@ -21,11 +23,15 @@ from wcs import fields from utilities import create_temporary_pub, clean_temporary_pub -def setup_module(module): - cleanup() - global pub +@pytest.fixture +def pub(): pub = create_temporary_pub() pub.substitutions.feed(pub) + req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'}) + pub.set_app_dir(req) + pub.site_options.set('options', 'working_day_calendar', '') + pub.site_options.write(open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w')) + return pub def teardown_module(module): @@ -73,7 +79,7 @@ def test_template(): assert tmpl.render({'foo': 'bar'}) == '[if-any foo][foo][endif]' -def test_now_and_today_variables(): +def test_now_and_today_variables(pub): # create a today string, verify it contains the year, at least today = Template('{{d}}').render({'d': datetime.date.today()}) assert datetime.date.today().strftime('%Y') in today @@ -689,3 +695,239 @@ def test_language_detect(): def test_datetime_in_past(value, expected): t = Template('{{ value|datetime_in_past }}') assert t.render({'value': value}) == str(expected) + + +def test_is_working_day_settings(settings, pub): + settings.WORKING_DAY_CALENDAR = None + t = Template('{{ value|is_working_day }}') + assert t.render({'value': '2020-07-15'}) == 'False' + t = Template('{{ value|is_working_day_with_saturday }}') + assert t.render({'value': '2020-07-15'}) == 'False' + + settings.WORKING_DAY_CALENDAR = '' + t = Template('{{ value|is_working_day }}') + assert t.render({'value': '2020-07-15'}) == 'False' + t = Template('{{ value|is_working_day_with_saturday }}') + assert t.render({'value': '2020-07-15'}) == 'False' + + settings.WORKING_DAY_CALENDAR = 'foobar' + t = Template('{{ value|is_working_day }}') + assert t.render({'value': '2020-07-15'}) == 'False' + t = Template('{{ value|is_working_day_with_saturday }}') + assert t.render({'value': '2020-07-15'}) == 'False' + + settings.WORKING_DAY_CALENDAR = 'workalendar.europe.France' + t = Template('{{ value|is_working_day }}') + assert t.render({'value': '2020-07-15'}) == 'True' + t = Template('{{ value|is_working_day_with_saturday }}') + assert t.render({'value': '2020-07-15'}) == 'True' + + pub.site_options.set('options', 'working_day_calendar', 'foobar') + pub.site_options.write(open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w')) + t = Template('{{ value|is_working_day }}') + assert t.render({'value': '2020-07-15'}) == 'False' + t = Template('{{ value|is_working_day_with_saturday }}') + assert t.render({'value': '2020-07-15'}) == 'False' + + settings.WORKING_DAY_CALENDAR = 'foobar' + pub.site_options.set('options', 'working_day_calendar', 'workalendar.europe.France') + pub.site_options.write(open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w')) + t = Template('{{ value|is_working_day }}') + assert t.render({'value': '2020-07-15'}) == 'True' + t = Template('{{ value|is_working_day_with_saturday }}') + assert t.render({'value': '2020-07-15'}) == 'True' + + +@pytest.mark.parametrize('value, expected', [ + (None, False), + ('', False), + ('foobar', False), + (42, False), + ('2020-07-14T12:01:03', False), + ('2020-07-15T12:01:03', True), + ('2020-07-14 02:03', False), + ('2020-07-15 02:03', True), + ('14/07/2020 02h03', False), + ('15/07/2020 02h03', True), + ('2020-07-14', False), + ('2020-07-15', True), + ('14/07/2020', False), + ('15/07/2020', True), + (datetime.datetime(2020, 7, 14, 12, 1, 3), False), + (datetime.datetime(2020, 7, 15, 12, 1, 3), True), + (datetime.date(2020, 7, 14), False), + (datetime.date(2020, 7, 15), True), +]) +def test_is_working_day(settings, value, expected): + settings.WORKING_DAY_CALENDAR = 'workalendar.europe.France' + t = Template('{{ value|is_working_day }}') + assert t.render({'value': value}) == str(expected) + t = Template('{{ value|is_working_day_with_saturday }}') + assert t.render({'value': value}) == str(expected) + + +def test_is_working_day_weekend(settings): + settings.WORKING_DAY_CALENDAR = 'workalendar.europe.France' + # check saturday + t = Template('{{ value|is_working_day }}') + assert t.render({'value': '2020-06-20'}) == 'False' + t = Template('{{ value|is_working_day_with_saturday }}') + assert t.render({'value': '2020-06-20'}) == 'True' + # check sunday + t = Template('{{ value|is_working_day }}') + assert t.render({'value': '2020-06-21'}) == 'False' + t = Template('{{ value|is_working_day_with_saturday }}') + assert t.render({'value': '2020-06-21'}) == 'False' + + +def test_add_working_days_settings(settings, pub): + settings.WORKING_DAY_CALENDAR = None + t = Template('{{ value|add_working_days:1 }}') + assert t.render({'value': '2020-07-13'}) == '' + t = Template('{{ value|add_working_days_with_saturday:1 }}') + assert t.render({'value': '2020-07-13'}) == '' + + settings.WORKING_DAY_CALENDAR = '' + t = Template('{{ value|add_working_days:1 }}') + assert t.render({'value': '2020-07-13'}) == '' + t = Template('{{ value|add_working_days_with_saturday:1 }}') + assert t.render({'value': '2020-07-13'}) == '' + + settings.WORKING_DAY_CALENDAR = 'foobar' + t = Template('{{ value|add_working_days:1 }}') + assert t.render({'value': '2020-07-13'}) == '' + t = Template('{{ value|add_working_days_with_saturday:1 }}') + assert t.render({'value': '2020-07-13'}) == '' + + settings.WORKING_DAY_CALENDAR = 'workalendar.europe.France' + t = Template('{{ value|add_working_days:1 }}') + assert t.render({'value': '2020-07-13'}) == '2020-07-15' + t = Template('{{ value|add_working_days_with_saturday:1 }}') + assert t.render({'value': '2020-07-13'}) == '2020-07-15' + + pub.site_options.set('options', 'working_day_calendar', 'foobar') + pub.site_options.write(open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w')) + t = Template('{{ value|add_working_days:1 }}') + assert t.render({'value': '2020-07-13'}) == '' + t = Template('{{ value|add_working_days_with_saturday:1 }}') + assert t.render({'value': '2020-07-13'}) == '' + + settings.WORKING_DAY_CALENDAR = 'foobar' + pub.site_options.set('options', 'working_day_calendar', 'workalendar.europe.France') + pub.site_options.write(open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w')) + t = Template('{{ value|add_working_days:1 }}') + assert t.render({'value': '2020-07-13'}) == '2020-07-15' + t = Template('{{ value|add_working_days_with_saturday:1 }}') + assert t.render({'value': '2020-07-13'}) == '2020-07-15' + + +def test_add_working_days_arg(settings): + settings.WORKING_DAY_CALENDAR = 'workalendar.europe.France' + t = Template('{{ value|add_working_days:"foobar" }}') + assert t.render({'value': '2020-07-13'}) == '' + t = Template('{{ value|add_working_days_with_saturday:"foobar" }}') + assert t.render({'value': '2020-07-13'}) == '' + t = Template('{{ value|add_working_days:2 }}') + assert t.render({'value': '2020-07-13'}) == '2020-07-16' + t = Template('{{ value|add_working_days_with_saturday:2 }}') + assert t.render({'value': '2020-07-13'}) == '2020-07-16' + + +@pytest.mark.parametrize('value, expected', [ + (None, ''), + ('', ''), + ('foobar', ''), + (42, ''), + ('2020-07-13T12:01:03', '2020-07-15'), + ('2020-07-13 02:03', '2020-07-15'), + ('13/07/2020 02h03', '2020-07-15'), + ('2020-07-13', '2020-07-15'), + ('13/07/2020', '2020-07-15'), + (datetime.datetime(2020, 7, 13, 12, 1, 3), '2020-07-15'), + (datetime.date(2020, 7, 13), '2020-07-15'), +]) +def test_add_working_days(settings, value, expected): + settings.WORKING_DAY_CALENDAR = 'workalendar.europe.France' + t = Template('{{ value|add_working_days:1 }}') + assert t.render({'value': value}) == str(expected) + t = Template('{{ value|add_working_days_with_saturday:1 }}') + assert t.render({'value': value}) == str(expected) + + +def test_add_working_days_weekend(settings): + settings.WORKING_DAY_CALENDAR = 'workalendar.europe.France' + t = Template('{{ value|add_working_days:1 }}') + assert t.render({'value': '2020-06-19'}) == '2020-06-22' + t = Template('{{ value|add_working_days_with_saturday:1 }}') + assert t.render({'value': '2020-06-19'}) == '2020-06-20' + + +def test_adjust_to_working_day_settings(settings, pub): + settings.WORKING_DAY_CALENDAR = None + t = Template('{{ value|adjust_to_working_day }}') + assert t.render({'value': '2020-07-13'}) == '' + t = Template('{{ value|adjust_to_working_day_with_saturday }}') + assert t.render({'value': '2020-07-13'}) == '' + + settings.WORKING_DAY_CALENDAR = '' + t = Template('{{ value|adjust_to_working_day }}') + assert t.render({'value': '2020-07-13'}) == '' + t = Template('{{ value|adjust_to_working_day_with_saturday }}') + assert t.render({'value': '2020-07-13'}) == '' + + settings.WORKING_DAY_CALENDAR = 'foobar' + t = Template('{{ value|adjust_to_working_day }}') + assert t.render({'value': '2020-07-13'}) == '' + t = Template('{{ value|adjust_to_working_day_with_saturday }}') + assert t.render({'value': '2020-07-13'}) == '' + + settings.WORKING_DAY_CALENDAR = 'workalendar.europe.France' + t = Template('{{ value|adjust_to_working_day }}') + assert t.render({'value': '2020-07-14'}) == '2020-07-15' + t = Template('{{ value|adjust_to_working_day_with_saturday }}') + assert t.render({'value': '2020-07-14'}) == '2020-07-15' + + pub.site_options.set('options', 'working_day_calendar', 'foobar') + pub.site_options.write(open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w')) + t = Template('{{ value|adjust_to_working_day }}') + assert t.render({'value': '2020-07-13'}) == '' + t = Template('{{ value|adjust_to_working_day_with_saturday }}') + assert t.render({'value': '2020-07-13'}) == '' + + settings.WORKING_DAY_CALENDAR = 'foobar' + pub.site_options.set('options', 'working_day_calendar', 'workalendar.europe.France') + pub.site_options.write(open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w')) + t = Template('{{ value|adjust_to_working_day }}') + assert t.render({'value': '2020-07-14'}) == '2020-07-15' + t = Template('{{ value|adjust_to_working_day_with_saturday }}') + assert t.render({'value': '2020-07-14'}) == '2020-07-15' + + +@pytest.mark.parametrize('value, expected', [ + (None, ''), + ('', ''), + ('foobar', ''), + (42, ''), + ('2020-07-14T12:01:03', '2020-07-15'), + ('2020-07-14 02:03', '2020-07-15'), + ('14/07/2020 02h03', '2020-07-15'), + ('2020-07-14', '2020-07-15'), + ('14/07/2020', '2020-07-15'), + (datetime.datetime(2020, 7, 14, 12, 1, 3), '2020-07-15'), + (datetime.date(2020, 7, 14), '2020-07-15'), + (datetime.date(2020, 7, 15), '2020-07-15'), +]) +def test_adjust_to_working_day(settings, value, expected): + settings.WORKING_DAY_CALENDAR = 'workalendar.europe.France' + t = Template('{{ value|adjust_to_working_day }}') + assert t.render({'value': value}) == str(expected) + t = Template('{{ value|adjust_to_working_day_with_saturday }}') + assert t.render({'value': value}) == str(expected) + + +def test_adjust_to_working_day_weekend(settings): + settings.WORKING_DAY_CALENDAR = 'workalendar.europe.France' + t = Template('{{ value|adjust_to_working_day }}') + assert t.render({'value': '2020-06-20'}) == '2020-06-22' + t = Template('{{ value|adjust_to_working_day_with_saturday }}') + assert t.render({'value': '2020-06-20'}) == '2020-06-20' diff --git a/tox.ini b/tox.ini index e81a8bfd2..f0e2359dd 100644 --- a/tox.ini +++ b/tox.ini @@ -30,6 +30,7 @@ deps = vobject qrcode Pillow<7.2 + workalendar python-magic docutils langdetect diff --git a/wcs/qommon/calendar.py b/wcs/qommon/calendar.py new file mode 100644 index 000000000..a2ae72ed8 --- /dev/null +++ b/wcs/qommon/calendar.py @@ -0,0 +1,41 @@ +# w.c.s. - web application for online forms +# Copyright (C) 2005-2020 Entr'ouvert +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, see . + +from django.utils.module_loading import import_string +from quixote import get_publisher + +try: + from workalendar.core import SUN +except ImportError: + SUN = None + + +def get_calendar(saturday_is_a_working_day=False): + # get calendar from settings + try: + calendar_class = import_string(get_publisher().get_working_day_calendar()) + except (AttributeError, ImportError): + return + + # saturday is not a working day, return this calendar + if not saturday_is_a_working_day: + return calendar_class() + + # saturday is a working day, build a custom calendar + class CalendarWithSaturday(calendar_class): + WEEKEND_DAYS = (SUN,) + + return CalendarWithSaturday() diff --git a/wcs/qommon/publisher.py b/wcs/qommon/publisher.py index 02ac3938b..7143ce0aa 100644 --- a/wcs/qommon/publisher.py +++ b/wcs/qommon/publisher.py @@ -892,6 +892,9 @@ class QommonPublisher(Publisher, object): url += '?key=%s' % key return url + def get_working_day_calendar(self): + return self.get_site_option('working_day_calendar') or settings.WORKING_DAY_CALENDAR + def get_supported_authentication_contexts(self): contexts = collections.OrderedDict() labels = { diff --git a/wcs/qommon/templatetags/qommon.py b/wcs/qommon/templatetags/qommon.py index edb43b0cd..708382210 100644 --- a/wcs/qommon/templatetags/qommon.py +++ b/wcs/qommon/templatetags/qommon.py @@ -39,6 +39,7 @@ from django.utils import six from django.utils.encoding import force_bytes, force_text from django.utils.safestring import mark_safe from django.utils.timezone import is_naive, make_aware +from wcs.qommon import calendar from wcs.qommon import evalutils from wcs.qommon import tokens from wcs.qommon.admin.texts import TextsDirectory @@ -281,6 +282,66 @@ def datetime_in_past(value): return value <= date_now +@register.filter(expects_localtime=True) +def is_working_day(value, saturday_is_a_working_day=False): + value = parse_date(value) + if not value: + return False + + cal = calendar.get_calendar(saturday_is_a_working_day=saturday_is_a_working_day) + if not cal: + return False + + return cal.is_working_day(value) + + +@register.filter(expects_localtime=True) +def is_working_day_with_saturday(value): + return is_working_day(value, saturday_is_a_working_day=True) + + +@register.filter(expects_localtime=True) +def add_working_days(value, arg, saturday_is_a_working_day=False): + value = parse_date(value) + if not value: + return '' + + cal = calendar.get_calendar(saturday_is_a_working_day=saturday_is_a_working_day) + if not cal: + return '' + + try: + return cal.add_working_days(value, int(arg)) + except ValueError: + return '' + + +@register.filter(expects_localtime=True) +def add_working_days_with_saturday(value, arg): + return add_working_days(value, arg, saturday_is_a_working_day=True) + + +@register.filter(expects_localtime=True) +def adjust_to_working_day(value, saturday_is_a_working_day=False): + value = parse_date(value) + if not value: + return '' + + cal = calendar.get_calendar(saturday_is_a_working_day=saturday_is_a_working_day) + if not cal: + return '' + + if cal.is_working_day(value): + return value + # return next working day + return cal.add_working_days(value, 1) + + +@register.filter(expects_localtime=True) +def adjust_to_working_day_with_saturday(value): + return adjust_to_working_day(value, saturday_is_a_working_day=True) + + @register.simple_tag def standard_text(text_id): return mark_safe(TextsDirectory.get_html_text(str(text_id))) diff --git a/wcs/settings.py b/wcs/settings.py index d3806aea8..8d383f520 100644 --- a/wcs/settings.py +++ b/wcs/settings.py @@ -188,6 +188,9 @@ DISABLE_CRON_JOBS = False # w.c.s. can have very large forms, in backoffice and frontoffice DATA_UPLOAD_MAX_NUMBER_FIELDS = 2000 # Django default is 1000 +# workalendar config +WORKING_DAY_CALENDAR = 'workalendar.europe.France' + local_settings_file = os.environ.get('WCS_SETTINGS_FILE', os.path.join(os.path.dirname(__file__), 'local_settings.py')) if os.path.exists(local_settings_file):