mirror of
https://github.com/pretix/pretix.git
synced 2026-05-05 15:14:04 +00:00
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:
@@ -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':
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 () {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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, {})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user