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
This commit is contained in:
Raphael Michel
2021-05-07 11:29:39 +02:00
committed by GitHub
parent 366395278e
commit b5e41f4c62
8 changed files with 114 additions and 11 deletions

View File

@@ -67,6 +67,14 @@ from pretix.helpers.jsonlogic_query import (
def _build_time(t=None, value=None, ev=None): def _build_time(t=None, value=None, ev=None):
if t == "custom": if t == "custom":
return dateutil.parser.parse(value) 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': elif t == 'date_from':
return ev.date_from return ev.date_from
elif t == 'date_to': elif t == 'date_to':
@@ -354,7 +362,15 @@ class SQLLogic:
if operator == 'buildTime': if operator == 'buildTime':
if values[0] == "custom": 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': elif values[0] == 'date_from':
return Coalesce( return Coalesce(
F(f'subevent__date_from'), F(f'subevent__date_from'),
@@ -382,7 +398,7 @@ class SQLLogic:
return int(values[1]) return int(values[1])
elif operator == 'var': elif operator == 'var':
if values[0] == 'now': if values[0] == 'now':
return Value(now()) return Value(now().astimezone(pytz.UTC))
elif values[0] == 'product': elif values[0] == 'product':
return F('item_id') return F('item_id')
elif values[0] == 'variation': elif values[0] == 'variation':

View File

@@ -119,6 +119,7 @@
<script type="text/javascript" src="{% static "d3/d3-drag.v2.js" %}"></script> <script type="text/javascript" src="{% static "d3/d3-drag.v2.js" %}"></script>
<script type="text/javascript" src="{% static "d3/d3-zoom.v2.js" %}"></script> <script type="text/javascript" src="{% static "d3/d3-zoom.v2.js" %}"></script>
<script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/datetimefield.vue' %}"></script> <script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/datetimefield.vue' %}"></script>
<script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/timefield.vue' %}"></script>
<script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/lookup-select2.vue' %}"></script> <script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/lookup-select2.vue' %}"></script>
<script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/checkin-rule.vue' %}"></script> <script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/checkin-rule.vue' %}"></script>
<script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/checkin-rules-editor.vue' %}"></script> <script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/checkin-rules-editor.vue' %}"></script>

View File

@@ -75,12 +75,14 @@ def convert_to_dnf(rules):
rules = _distribute_or_over_and(rules) rules = _distribute_or_over_and(rules)
operator = list(rules.keys())[0] operator = list(rules.keys())[0]
values = rules[operator] values = rules[operator]
no_list = False
if not isinstance(values, list): if not isinstance(values, list):
values = [values] values = [values]
no_list = True
rules = { rules = {
operator: [ operator: [
convert_to_dnf(v) for v in values 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: if old_rules == rules:
break break

View File

@@ -99,7 +99,8 @@ $(document).ready(function () {
date_from: gettext('Event start'), date_from: gettext('Event start'),
date_to: gettext('Event end'), date_to: gettext('Event end'),
date_admission: gettext('Event admission'), 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)'), date_tolerance: gettext('Tolerance (minutes)'),
condition_add: gettext('Add condition'), condition_add: gettext('Add condition'),
minutes: gettext('minutes'), minutes: gettext('minutes'),
@@ -119,4 +120,4 @@ $(document).ready(function () {
}, },
} }
}) })
}); });

View File

@@ -27,11 +27,14 @@
<option value="date_to">{{texts.date_to}}</option> <option value="date_to">{{texts.date_to}}</option>
<option value="date_admission">{{texts.date_admission}}</option> <option value="date_admission">{{texts.date_admission}}</option>
<option value="custom">{{texts.date_custom}}</option> <option value="custom">{{texts.date_custom}}</option>
<option value="customtime">{{texts.date_customtime}}</option>
</select> </select>
<datetimefield v-if="vartype == 'datetime' && timeType == 'custom'" :value="timeValue" <datetimefield v-if="vartype == 'datetime' && timeType == 'custom'" :value="timeValue"
v-on:input="setTimeValue"></datetimefield> v-on:input="setTimeValue"></datetimefield>
<timefield v-if="vartype == 'datetime' && timeType == 'customtime'" :value="timeValue"
v-on:input="setTimeValue"></timefield>
<input class="form-control" required type="number" <input class="form-control" required type="number"
v-if="vartype == 'datetime' && timeType && timeType != 'custom'" v-bind:value="timeTolerance" v-if="vartype == 'datetime' && timeType && timeType != 'customtime' && timeType != 'custom'" v-bind:value="timeTolerance"
v-on:input="setTimeTolerance" :placeholder="texts.date_tolerance"> v-on:input="setTimeTolerance" :placeholder="texts.date_tolerance">
<input class="form-control" required type="number" v-if="vartype == 'int' && cardinality > 1" <input class="form-control" required type="number" v-if="vartype == 'int' && cardinality > 1"
v-bind:value="rightoperand" v-on:input="setRightOperandNumber"> v-bind:value="rightoperand" v-on:input="setRightOperandNumber">
@@ -56,6 +59,7 @@
components: { components: {
LookupSelect2: LookupSelect2.default, LookupSelect2: LookupSelect2.default,
Datetimefield: Datetimefield.default, Datetimefield: Datetimefield.default,
Timefield: Timefield.default,
}, },
props: { props: {
rule: Object, rule: Object,

View File

@@ -0,0 +1,55 @@
<template>
<input class="form-control">
</template>
<script>
export default {
props: ["required", "value"],
template: (''),
mounted: function () {
var vm = this;
var multiple = this.multiple;
$(this.$el)
.datetimepicker(this.opts())
.trigger("change")
.on("dp.change", function (e) {
vm.$emit("input", $(this).data('DateTimePicker').date().format("HH:mm:ss"));
});
if (!vm.value) {
$(this.$el).data("DateTimePicker").viewDate(moment().hour(0).minute(0).second(0).millisecond(0));
} else {
$(this.$el).data("DateTimePicker").date(vm.value);
}
},
methods: {
opts: function () {
return {
format: $("body").attr("data-timeformat"),
locale: $("body").attr("data-datetimelocale"),
useCurrent: false,
showClear: this.required,
icons: {
time: 'fa fa-clock-o',
date: 'fa fa-calendar',
up: 'fa fa-chevron-up',
down: 'fa fa-chevron-down',
previous: 'fa fa-chevron-left',
next: 'fa fa-chevron-right',
today: 'fa fa-screenshot',
clear: 'fa fa-trash',
close: 'fa fa-remove'
}
};
}
},
watch: {
value: function (val) {
$(this.$el).data('DateTimePicker').date(val);
},
},
destroyed: function () {
$(this.$el)
.off()
.datetimepicker("destroy");
}
}
</script>

View File

@@ -22,6 +22,9 @@
<span v-if="rightoperand.buildTime[0] === 'custom'"> <span v-if="rightoperand.buildTime[0] === 'custom'">
{{ df(rightoperand.buildTime[1]) }} {{ df(rightoperand.buildTime[1]) }}
</span> </span>
<span v-else-if="rightoperand.buildTime[0] === 'customtime'">
{{ tf(rightoperand.buildTime[1]) }}
</span>
<span v-else> <span v-else>
{{ this.$root.texts[rightoperand.buildTime[0]] }} {{ this.$root.texts[rightoperand.buildTime[0]] }}
</span> </span>
@@ -139,6 +142,10 @@
df (val) { df (val) {
const format = $("body").attr("data-datetimeformat") const format = $("body").attr("data-datetimeformat")
return moment(val).format(format) return moment(val).format(format)
},
tf (val) {
const format = $("body").attr("data-timeformat")
return moment(val).format(format)
} }
}, },
} }

View File

@@ -25,7 +25,7 @@ from decimal import Decimal
import pytest import pytest
from django.conf import settings from django.conf import settings
from django.utils.timezone import now from django.utils.timezone import now, override
from django_scopes import scope from django_scopes import scope
from freezegun import freeze_time 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): def test_rules_time_isafter_custom_time(event, position, clist):
# Ticket is valid starting at a custom time # Ticket is valid starting at a custom time
event.settings.timezone = 'Europe/Berlin' 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() 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() assert not OrderPosition.objects.filter(SQLLogic(clist).apply(clist.rules), pk=position.pk).exists()
with pytest.raises(CheckInError) as excinfo: with pytest.raises(CheckInError) as excinfo:
perform_checkin(position, clist, {}) perform_checkin(position, clist, {})
assert excinfo.value.code == 'rules' 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() assert OrderPosition.objects.filter(SQLLogic(clist).apply(clist.rules), pk=position.pk).exists()
perform_checkin(position, clist, {}) perform_checkin(position, clist, {})