From b5e41f4c62f3e4d4d50277c2e544c341e2237292 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Fri, 7 May 2021 11:29:39 +0200 Subject: [PATCH] Check-in rules: Allow to check for time of day (#2061) * Add "customtime" option * Fix time picker output format * Fix bug in bool_alg * Fix test --- src/pretix/base/services/checkin.py | 20 ++++++- .../pretixcontrol/checkin/list_edit.html | 1 + src/pretix/helpers/jsonlogic_boolalg.py | 4 +- .../pretixcontrol/js/ui/checkinrules.js | 5 +- .../js/ui/checkinrules/checkin-rule.vue | 6 +- .../js/ui/checkinrules/timefield.vue | 55 +++++++++++++++++++ .../js/ui/checkinrules/viz-node.vue | 7 +++ src/tests/base/test_checkin.py | 27 +++++++-- 8 files changed, 114 insertions(+), 11 deletions(-) create mode 100644 src/pretix/static/pretixcontrol/js/ui/checkinrules/timefield.vue diff --git a/src/pretix/base/services/checkin.py b/src/pretix/base/services/checkin.py index d8d815bebf..cdd36eab5b 100644 --- a/src/pretix/base/services/checkin.py +++ b/src/pretix/base/services/checkin.py @@ -67,6 +67,14 @@ from pretix.helpers.jsonlogic_query import ( def _build_time(t=None, value=None, ev=None): if t == "custom": return dateutil.parser.parse(value) + elif t == "customtime": + parsed = dateutil.parser.parse(value) + return now().astimezone(ev.timezone).replace( + hour=parsed.hour, + minute=parsed.minute, + second=parsed.second, + microsecond=parsed.microsecond, + ) elif t == 'date_from': return ev.date_from elif t == 'date_to': @@ -354,7 +362,15 @@ class SQLLogic: if operator == 'buildTime': if values[0] == "custom": - return Value(dateutil.parser.parse(values[1])) + return Value(dateutil.parser.parse(values[1]).astimezone(pytz.UTC)) + elif values[0] == "customtime": + parsed = dateutil.parser.parse(values[1]) + return Value(now().astimezone(self.list.event.timezone).replace( + hour=parsed.hour, + minute=parsed.minute, + second=parsed.second, + microsecond=parsed.microsecond, + ).astimezone(pytz.UTC)) elif values[0] == 'date_from': return Coalesce( F(f'subevent__date_from'), @@ -382,7 +398,7 @@ class SQLLogic: return int(values[1]) elif operator == 'var': if values[0] == 'now': - return Value(now()) + return Value(now().astimezone(pytz.UTC)) elif values[0] == 'product': return F('item_id') elif values[0] == 'variation': diff --git a/src/pretix/control/templates/pretixcontrol/checkin/list_edit.html b/src/pretix/control/templates/pretixcontrol/checkin/list_edit.html index 2120e5dff8..54ab0e45df 100644 --- a/src/pretix/control/templates/pretixcontrol/checkin/list_edit.html +++ b/src/pretix/control/templates/pretixcontrol/checkin/list_edit.html @@ -119,6 +119,7 @@ + diff --git a/src/pretix/helpers/jsonlogic_boolalg.py b/src/pretix/helpers/jsonlogic_boolalg.py index 58162d840a..bd9a87157e 100644 --- a/src/pretix/helpers/jsonlogic_boolalg.py +++ b/src/pretix/helpers/jsonlogic_boolalg.py @@ -75,12 +75,14 @@ def convert_to_dnf(rules): rules = _distribute_or_over_and(rules) operator = list(rules.keys())[0] values = rules[operator] + no_list = False if not isinstance(values, list): values = [values] + no_list = True rules = { operator: [ convert_to_dnf(v) for v in values - ] if len(values) > 1 else convert_to_dnf(values[0]) + ] if not no_list else convert_to_dnf(values[0]) } if old_rules == rules: break diff --git a/src/pretix/static/pretixcontrol/js/ui/checkinrules.js b/src/pretix/static/pretixcontrol/js/ui/checkinrules.js index b1ca716957..68d18bbade 100644 --- a/src/pretix/static/pretixcontrol/js/ui/checkinrules.js +++ b/src/pretix/static/pretixcontrol/js/ui/checkinrules.js @@ -99,7 +99,8 @@ $(document).ready(function () { date_from: gettext('Event start'), date_to: gettext('Event end'), date_admission: gettext('Event admission'), - date_custom: gettext('custom time'), + date_custom: gettext('custom date and time'), + date_customtime: gettext('custom time'), date_tolerance: gettext('Tolerance (minutes)'), condition_add: gettext('Add condition'), minutes: gettext('minutes'), @@ -119,4 +120,4 @@ $(document).ready(function () { }, } }) -}); \ No newline at end of file +}); diff --git a/src/pretix/static/pretixcontrol/js/ui/checkinrules/checkin-rule.vue b/src/pretix/static/pretixcontrol/js/ui/checkinrules/checkin-rule.vue index 44176683e2..035edc3dec 100644 --- a/src/pretix/static/pretixcontrol/js/ui/checkinrules/checkin-rule.vue +++ b/src/pretix/static/pretixcontrol/js/ui/checkinrules/checkin-rule.vue @@ -27,11 +27,14 @@ + + @@ -56,6 +59,7 @@ components: { LookupSelect2: LookupSelect2.default, Datetimefield: Datetimefield.default, + Timefield: Timefield.default, }, props: { rule: Object, diff --git a/src/pretix/static/pretixcontrol/js/ui/checkinrules/timefield.vue b/src/pretix/static/pretixcontrol/js/ui/checkinrules/timefield.vue new file mode 100644 index 0000000000..93641dc70f --- /dev/null +++ b/src/pretix/static/pretixcontrol/js/ui/checkinrules/timefield.vue @@ -0,0 +1,55 @@ + + diff --git a/src/pretix/static/pretixcontrol/js/ui/checkinrules/viz-node.vue b/src/pretix/static/pretixcontrol/js/ui/checkinrules/viz-node.vue index 546783821e..94b81af99b 100644 --- a/src/pretix/static/pretixcontrol/js/ui/checkinrules/viz-node.vue +++ b/src/pretix/static/pretixcontrol/js/ui/checkinrules/viz-node.vue @@ -22,6 +22,9 @@ {{ df(rightoperand.buildTime[1]) }} + + {{ tf(rightoperand.buildTime[1]) }} + {{ this.$root.texts[rightoperand.buildTime[0]] }} @@ -139,6 +142,10 @@ df (val) { const format = $("body").attr("data-datetimeformat") return moment(val).format(format) + }, + tf (val) { + const format = $("body").attr("data-timeformat") + return moment(val).format(format) } }, } diff --git a/src/tests/base/test_checkin.py b/src/tests/base/test_checkin.py index 11536bcb5d..09d7986234 100644 --- a/src/tests/base/test_checkin.py +++ b/src/tests/base/test_checkin.py @@ -25,7 +25,7 @@ from decimal import Decimal import pytest from django.conf import settings -from django.utils.timezone import now +from django.utils.timezone import now, override from django_scopes import scope from freezegun import freeze_time @@ -621,16 +621,33 @@ def test_rules_time_isbefore_with_tolerance(event, position, clist): def test_rules_time_isafter_custom_time(event, position, clist): # Ticket is valid starting at a custom time event.settings.timezone = 'Europe/Berlin' - clist.rules = {"isAfter": [{"var": "now"}, {"buildTime": ["custom", "2020-01-01T22:00:00.000Z"]}, None]} + clist.rules = {"isAfter": [{"var": "now"}, {"buildTime": ["customtime", "22:00:00"]}, None]} clist.save() - with freeze_time("2020-01-01 21:55:00"): + with freeze_time("2020-01-01 21:55:00+01:00"), override(event.timezone): assert not OrderPosition.objects.filter(SQLLogic(clist).apply(clist.rules), pk=position.pk).exists() with pytest.raises(CheckInError) as excinfo: perform_checkin(position, clist, {}) assert excinfo.value.code == 'rules' - assert 'Only allowed after 23:00' in str(excinfo.value) + assert 'Only allowed after 22:00' in str(excinfo.value) - with freeze_time("2020-01-01 22:05:00"): + with freeze_time("2020-01-01 22:05:00+01:00"): + assert OrderPosition.objects.filter(SQLLogic(clist).apply(clist.rules), pk=position.pk).exists() + perform_checkin(position, clist, {}) + + +@pytest.mark.django_db +def test_rules_time_isafter_custom_datetime(event, position, clist): + # Ticket is valid starting at a custom time + event.settings.timezone = 'Europe/Berlin' + clist.rules = {"isAfter": [{"var": "now"}, {"buildTime": ["custom", "2020-01-01T23:00:00.000+01:00"]}, None]} + clist.save() + with freeze_time("2020-01-01 21:55:00+00:00"): + assert not OrderPosition.objects.filter(SQLLogic(clist).apply(clist.rules), pk=position.pk).exists() + with pytest.raises(CheckInError) as excinfo: + perform_checkin(position, clist, {}) + assert excinfo.value.code == 'rules' + + with freeze_time("2020-01-01 22:05:00+00:00"): assert OrderPosition.objects.filter(SQLLogic(clist).apply(clist.rules), pk=position.pk).exists() perform_checkin(position, clist, {})