forked from CGM_Public/pretix_original
Check-in: Add rule for number of days with entries since (#3808)
This commit is contained in:
@@ -280,7 +280,8 @@ class CheckinList(LoggedModel):
|
||||
'<', '<=', '>', '>=', '==', '!=', 'inList', 'isBefore', 'isAfter', 'or', 'and'
|
||||
}
|
||||
allowed_operators = top_level_operators | {
|
||||
'buildTime', 'objectList', 'lookup', 'var', 'entries_since', 'entries_before'
|
||||
'buildTime', 'objectList', 'lookup', 'var', 'entries_since', 'entries_before', 'entries_days_since',
|
||||
'entries_days_before',
|
||||
}
|
||||
allowed_vars = {
|
||||
'product', 'variation', 'now', 'now_isoweekday', 'entries_number', 'entries_today', 'entries_days',
|
||||
@@ -309,7 +310,7 @@ class CheckinList(LoggedModel):
|
||||
raise ValidationError(f'Logic variable "{values[0]}" is currently not allowed.')
|
||||
return rules
|
||||
|
||||
if operator in ('entries_since', 'entries_before'):
|
||||
if operator in ('entries_since', 'entries_before', 'entries_days_since', 'entries_days_before'):
|
||||
if len(values) != 1 or "buildTime" not in values[0]:
|
||||
raise ValidationError(f'Operator "{operator}" takes exactly one "buildTime" argument.')
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
|
||||
'var': values[0]["var"],
|
||||
'rhs': values[1:],
|
||||
}
|
||||
elif "entries_since" in values[0] or "entries_before" in values[0]:
|
||||
elif any(t in values[0] for t in ("entries_since", "entries_before", "entries_days_since", "entries_days_before")):
|
||||
_var_explanations[new_var_name] = {
|
||||
'operator': operator,
|
||||
'var': values[0],
|
||||
@@ -280,11 +280,13 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
|
||||
var_weights[vname] = (500, 0)
|
||||
var_texts[vname] = _('Wrong entrance gate')
|
||||
elif var in ('entries_number', 'entries_today', 'entries_days', 'minutes_since_last_entry', 'minutes_since_first_entry', 'now_isoweekday') \
|
||||
or (isinstance(var, dict) and ("entries_since" in var or "entries_before" in var)):
|
||||
or (isinstance(var, dict) and any(t in var for t in ("entries_since", "entries_before", "entries_days_since", "entries_days_before"))):
|
||||
w = {
|
||||
'minutes_since_first_entry': 80,
|
||||
'minutes_since_last_entry': 90,
|
||||
'entries_days': 100,
|
||||
'entries_days_since': 105,
|
||||
'entries_days_before': 105,
|
||||
'entries_since': 110,
|
||||
'entries_before': 110,
|
||||
'entries_number': 120,
|
||||
@@ -307,10 +309,12 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
|
||||
'entries_today': _('number of entries today'),
|
||||
'entries_since': _('number of entries since {datetime}'),
|
||||
'entries_before': _('number of entries before {datetime}'),
|
||||
'entries_days_since': _('number of days with an entry since {datetime}'),
|
||||
'entries_days_before': _('number of days with an entry before {datetime}'),
|
||||
'now_isoweekday': _('week day'),
|
||||
}
|
||||
|
||||
if isinstance(var, dict) and ("entries_since" in var or "entries_before" in var):
|
||||
if isinstance(var, dict) and any(t in var for t in ("entries_since", "entries_before", "entries_days_since", "entries_days_before")):
|
||||
varname = list(var.keys())[0]
|
||||
cutoff = _build_time(*var[varname][0]['buildTime'], ev=ev, now_dt=now_dt).astimezone(ev.timezone)
|
||||
if abs(now_dt - cutoff) < timedelta(hours=12):
|
||||
@@ -410,6 +414,8 @@ def _get_logic_environment(ev, rule_data, now_dt):
|
||||
logic.add_operation('isAfter', lambda t1, t2, tol=None: is_before(t2, t1, tol))
|
||||
logic.add_operation('entries_since', lambda t1: rule_data.entries_since(t1))
|
||||
logic.add_operation('entries_before', lambda t1: rule_data.entries_before(t1))
|
||||
logic.add_operation('entries_days_since', lambda t1: rule_data.entries_days_since(t1))
|
||||
logic.add_operation('entries_days_before', lambda t1: rule_data.entries_days_before(t1))
|
||||
return logic
|
||||
|
||||
|
||||
@@ -467,6 +473,32 @@ class LazyRuleVars:
|
||||
self.__cache['entries_before', cutoff] = self._position.checkins.filter(type=Checkin.TYPE_ENTRY, list=self._clist, datetime__lt=cutoff).count()
|
||||
return self.__cache['entries_before', cutoff]
|
||||
|
||||
def entries_days_since(self, cutoff):
|
||||
tz = self._clist.event.timezone
|
||||
with override(tz):
|
||||
if ('entries_days_since', cutoff) not in self.__cache:
|
||||
self.__cache['entries_days_since', cutoff] = self._position.checkins.filter(
|
||||
type=Checkin.TYPE_ENTRY,
|
||||
list=self._clist,
|
||||
datetime__gte=cutoff
|
||||
).annotate(
|
||||
day=TruncDate('datetime', tzinfo=tz)
|
||||
).values('day').distinct().count()
|
||||
return self.__cache['entries_days_since', cutoff]
|
||||
|
||||
def entries_days_before(self, cutoff):
|
||||
tz = self._clist.event.timezone
|
||||
with override(tz):
|
||||
if ('entries_days_before', cutoff) not in self.__cache:
|
||||
self.__cache['entries_days_before', cutoff] = self._position.checkins.filter(
|
||||
type=Checkin.TYPE_ENTRY,
|
||||
list=self._clist,
|
||||
datetime__lt=cutoff
|
||||
).annotate(
|
||||
day=TruncDate('datetime', tzinfo=tz)
|
||||
).values('day').distinct().count()
|
||||
return self.__cache['entries_days_before', cutoff]
|
||||
|
||||
@cached_property
|
||||
def entries_days(self):
|
||||
tz = self._clist.event.timezone
|
||||
@@ -533,7 +565,8 @@ class SQLLogic:
|
||||
"isBefore": partial(self.comparison_to_q, operator=LowerThan, modifier=partial(tolerance, sign=1)),
|
||||
"isAfter": partial(self.comparison_to_q, operator=GreaterThan, modifier=partial(tolerance, sign=-1)),
|
||||
}
|
||||
self.expression_ops = {'buildTime', 'objectList', 'lookup', 'var', 'entries_since', 'entries_before'}
|
||||
self.expression_ops = {'buildTime', 'objectList', 'lookup', 'var', 'entries_since', 'entries_before',
|
||||
'entries_days_since', 'entries_days_before'}
|
||||
|
||||
def operation_to_expression(self, rule):
|
||||
if not isinstance(rule, dict):
|
||||
@@ -611,6 +644,42 @@ class SQLLogic:
|
||||
Value(0),
|
||||
output_field=IntegerField()
|
||||
)
|
||||
elif operator == 'entries_days_since':
|
||||
tz = self.list.event.timezone
|
||||
return Coalesce(
|
||||
Subquery(
|
||||
Checkin.objects.filter(
|
||||
position_id=OuterRef('pk'),
|
||||
type=Checkin.TYPE_ENTRY,
|
||||
list_id=self.list.pk,
|
||||
datetime__gte=self.operation_to_expression(values[0]),
|
||||
).annotate(
|
||||
day=TruncDate('datetime', tzinfo=tz)
|
||||
).values('position_id').order_by().annotate(
|
||||
c=Count('day', distinct=True)
|
||||
).values('c')
|
||||
),
|
||||
Value(0),
|
||||
output_field=IntegerField()
|
||||
)
|
||||
elif operator == 'entries_days_before':
|
||||
tz = self.list.event.timezone
|
||||
return Coalesce(
|
||||
Subquery(
|
||||
Checkin.objects.filter(
|
||||
position_id=OuterRef('pk'),
|
||||
type=Checkin.TYPE_ENTRY,
|
||||
list_id=self.list.pk,
|
||||
datetime__lt=self.operation_to_expression(values[0]),
|
||||
).annotate(
|
||||
day=TruncDate('datetime', tzinfo=tz)
|
||||
).values('position_id').order_by().annotate(
|
||||
c=Count('day', distinct=True)
|
||||
).values('c')
|
||||
),
|
||||
Value(0),
|
||||
output_field=IntegerField()
|
||||
)
|
||||
elif operator == 'var':
|
||||
if values[0] == 'now':
|
||||
return Value(now().astimezone(timezone.utc))
|
||||
|
||||
@@ -129,6 +129,14 @@ $(function () {
|
||||
'label': gettext('Number of days with a previous entry'),
|
||||
'type': 'int',
|
||||
},
|
||||
'entries_days_since': {
|
||||
'label': gettext('Number of days with a previous entry since'),
|
||||
'type': 'int_by_datetime',
|
||||
},
|
||||
'entries_days_before': {
|
||||
'label': gettext('Number of days with a previous entry before'),
|
||||
'type': 'int_by_datetime',
|
||||
},
|
||||
'minutes_since_last_entry': {
|
||||
'label': gettext('Minutes since last entry (-1 on first entry)'),
|
||||
'type': 'int',
|
||||
|
||||
@@ -93,6 +93,12 @@
|
||||
if (this.rule[op][0]["entries_before"]) {
|
||||
return "entries_before";
|
||||
}
|
||||
if (this.rule[op][0]["entries_days_since"]) {
|
||||
return "entries_days_since";
|
||||
}
|
||||
if (this.rule[op][0]["entries_days_before"]) {
|
||||
return "entries_days_before";
|
||||
}
|
||||
return this.rule[op][0]["var"];
|
||||
} else {
|
||||
return null;
|
||||
|
||||
@@ -166,6 +166,12 @@
|
||||
if (this.node.rule[op][0]["entries_before"]) {
|
||||
return "entries_before";
|
||||
}
|
||||
if (this.node.rule[op][0]["entries_days_since"]) {
|
||||
return "entries_days_since";
|
||||
}
|
||||
if (this.node.rule[op][0]["entries_days_before"]) {
|
||||
return "entries_days_before";
|
||||
}
|
||||
return this.node.rule[op][0]["var"];
|
||||
} else {
|
||||
return "";
|
||||
|
||||
@@ -846,6 +846,85 @@ def test_rules_entries_before(event, position, clist):
|
||||
assert 'Minimum number of entries before 23:00 exceeded' in str(excinfo.value)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_rules_entries_days_since(event, position, clist):
|
||||
# Ticket is valid once before X and on one day after X
|
||||
event.settings.timezone = 'Europe/Berlin'
|
||||
clist.allow_multiple_entries = True
|
||||
clist.rules = {
|
||||
"or": [
|
||||
{"<=": [{"var": "entries_number"}, 0]},
|
||||
{"and": [
|
||||
{"isAfter": [{"var": "now"}, {"buildTime": ["custom", "2020-01-01T23:00:00.000+01:00"]}, 0]},
|
||||
{"or": [
|
||||
{">": [{"var": "entries_today"}, 0]},
|
||||
{"<=": [{"entries_days_since": [{"buildTime": ["custom", "2020-01-01T23:00:00.000+01:00"]}]}, 0]},
|
||||
]}
|
||||
]},
|
||||
],
|
||||
}
|
||||
clist.save()
|
||||
with freeze_time("2020-01-01 22:00:00+01:00"):
|
||||
assert OrderPosition.objects.filter(SQLLogic(clist).apply(clist.rules), pk=position.pk).exists()
|
||||
perform_checkin(position, clist, {})
|
||||
|
||||
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 'Maximum number of entries exceeded' in str(excinfo.value)
|
||||
|
||||
with freeze_time("2020-01-02 23:10:00+01:00"):
|
||||
assert OrderPosition.objects.filter(SQLLogic(clist).apply(clist.rules), pk=position.pk).exists()
|
||||
perform_checkin(position, clist, {})
|
||||
|
||||
perform_checkin(position, clist, {})
|
||||
assert OrderPosition.objects.filter(SQLLogic(clist).apply(clist.rules), pk=position.pk).exists()
|
||||
|
||||
with freeze_time("2020-01-03 23:10:00+01: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'
|
||||
assert 'Maximum number of days with an entry since 2020-01-01 23:00 exceeded' in str(excinfo.value)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_rules_entries_days_before(event, position, clist):
|
||||
# Ticket is valid after 23:00 only if people already showed up on two days before
|
||||
event.settings.timezone = 'Europe/Berlin'
|
||||
clist.allow_multiple_entries = True
|
||||
clist.rules = {
|
||||
"or": [
|
||||
{"isBefore": [{"var": "now"}, {"buildTime": ["custom", "2020-01-01T23:00:00.000+01:00"]}, 0]},
|
||||
{"and": [
|
||||
{"isAfter": [{"var": "now"}, {"buildTime": ["custom", "2020-01-01T23:00:00.000+01:00"]}, 0]},
|
||||
{">=": [{"entries_days_before": [{"buildTime": ["custom", "2020-01-01T23:00:00.000+01:00"]}]}, 2]},
|
||||
]},
|
||||
],
|
||||
}
|
||||
clist.save()
|
||||
|
||||
with freeze_time("2019-12-30 22:00:00+01:00"):
|
||||
assert OrderPosition.objects.filter(SQLLogic(clist).apply(clist.rules), pk=position.pk).exists()
|
||||
perform_checkin(position, clist, {})
|
||||
|
||||
with freeze_time("2020-01-02 23:10:00+01: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'
|
||||
assert 'Minimum number of days with an entry before 2020-01-01 23:00 exceeded.' in str(excinfo.value)
|
||||
|
||||
with freeze_time("2019-12-31 22:00:00+01:00"):
|
||||
assert OrderPosition.objects.filter(SQLLogic(clist).apply(clist.rules), pk=position.pk).exists()
|
||||
perform_checkin(position, clist, {})
|
||||
|
||||
with freeze_time("2020-01-02 23:10: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_tolerance(event, position, clist):
|
||||
# Ticket is valid starting 10 minutes before admission time
|
||||
|
||||
Reference in New Issue
Block a user