mirror of
https://github.com/pretix/pretix.git
synced 2026-08-24 13:02:00 +00:00
Check-in rules: New variables (#3521)
This commit is contained in:
@@ -278,6 +278,7 @@ with scopes_disabled():
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.checkinlist = kwargs.pop('checkinlist')
|
self.checkinlist = kwargs.pop('checkinlist')
|
||||||
|
self.gate = kwargs.pop('gate')
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def has_checkin_qs(self, queryset, name, value):
|
def has_checkin_qs(self, queryset, name, value):
|
||||||
@@ -287,7 +288,7 @@ with scopes_disabled():
|
|||||||
if not self.checkinlist.rules:
|
if not self.checkinlist.rules:
|
||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
SQLLogic(self.checkinlist).apply(self.checkinlist.rules)
|
SQLLogic(self.checkinlist, self.gate).apply(self.checkinlist.rules)
|
||||||
).filter(
|
).filter(
|
||||||
Q(valid_from__isnull=True) | Q(valid_from__lte=now()),
|
Q(valid_from__isnull=True) | Q(valid_from__lte=now()),
|
||||||
Q(valid_until__isnull=True) | Q(valid_until__gte=now()),
|
Q(valid_until__isnull=True) | Q(valid_until__gte=now()),
|
||||||
@@ -409,7 +410,7 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
|
|||||||
|
|
||||||
def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force, checkin_type, ignore_unpaid, nonce,
|
def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force, checkin_type, ignore_unpaid, nonce,
|
||||||
untrusted_input, user, auth, expand, pdf_data, request, questions_supported, canceled_supported,
|
untrusted_input, user, auth, expand, pdf_data, request, questions_supported, canceled_supported,
|
||||||
source_type='barcode', legacy_url_support=False, simulate=False):
|
source_type='barcode', legacy_url_support=False, simulate=False, gate=None):
|
||||||
if not checkinlists:
|
if not checkinlists:
|
||||||
raise ValidationError('No check-in list passed.')
|
raise ValidationError('No check-in list passed.')
|
||||||
|
|
||||||
@@ -417,7 +418,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
|||||||
prefetch_related_objects([cl for cl in checkinlists if not cl.all_products], 'limit_products')
|
prefetch_related_objects([cl for cl in checkinlists if not cl.all_products], 'limit_products')
|
||||||
|
|
||||||
device = auth if isinstance(auth, Device) else None
|
device = auth if isinstance(auth, Device) else None
|
||||||
gate = auth.gate if isinstance(auth, Device) else None
|
gate = gate or (auth.gate if isinstance(auth, Device) else None)
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'request': request,
|
'request': request,
|
||||||
@@ -672,6 +673,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
|||||||
raw_source_type=source_type,
|
raw_source_type=source_type,
|
||||||
from_revoked_secret=from_revoked_secret,
|
from_revoked_secret=from_revoked_secret,
|
||||||
simulate=simulate,
|
simulate=simulate,
|
||||||
|
gate=gate,
|
||||||
)
|
)
|
||||||
except RequiredQuestionsError as e:
|
except RequiredQuestionsError as e:
|
||||||
return Response({
|
return Response({
|
||||||
@@ -770,6 +772,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
def get_filterset_kwargs(self):
|
def get_filterset_kwargs(self):
|
||||||
return {
|
return {
|
||||||
'checkinlist': self.checkinlist,
|
'checkinlist': self.checkinlist,
|
||||||
|
'gate': self.request.auth.gate if isinstance(self.request.auth, Device) else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
|
|||||||
@@ -265,16 +265,16 @@ class CheckinList(LoggedModel):
|
|||||||
# * in pretix.helpers.jsonlogic_boolalg
|
# * in pretix.helpers.jsonlogic_boolalg
|
||||||
# * in checkinrules.js
|
# * in checkinrules.js
|
||||||
# * in libpretixsync
|
# * in libpretixsync
|
||||||
# * in pretixscan-ios (in the future)
|
# * in pretixscan-ios
|
||||||
top_level_operators = {
|
top_level_operators = {
|
||||||
'<', '<=', '>', '>=', '==', '!=', 'inList', 'isBefore', 'isAfter', 'or', 'and'
|
'<', '<=', '>', '>=', '==', '!=', 'inList', 'isBefore', 'isAfter', 'or', 'and'
|
||||||
}
|
}
|
||||||
allowed_operators = top_level_operators | {
|
allowed_operators = top_level_operators | {
|
||||||
'buildTime', 'objectList', 'lookup', 'var',
|
'buildTime', 'objectList', 'lookup', 'var', 'entries_since', 'entries_before'
|
||||||
}
|
}
|
||||||
allowed_vars = {
|
allowed_vars = {
|
||||||
'product', 'variation', 'now', 'now_isoweekday', 'entries_number', 'entries_today', 'entries_days',
|
'product', 'variation', 'now', 'now_isoweekday', 'entries_number', 'entries_today', 'entries_days',
|
||||||
'minutes_since_last_entry', 'minutes_since_first_entry',
|
'minutes_since_last_entry', 'minutes_since_first_entry', 'gate',
|
||||||
}
|
}
|
||||||
if not rules or not isinstance(rules, dict):
|
if not rules or not isinstance(rules, dict):
|
||||||
return rules
|
return rules
|
||||||
@@ -299,6 +299,10 @@ class CheckinList(LoggedModel):
|
|||||||
raise ValidationError(f'Logic variable "{values[0]}" is currently not allowed.')
|
raise ValidationError(f'Logic variable "{values[0]}" is currently not allowed.')
|
||||||
return rules
|
return rules
|
||||||
|
|
||||||
|
if operator in ('entries_since', 'entries_before'):
|
||||||
|
if len(values) != 1 or "buildTime" not in values[0]:
|
||||||
|
raise ValidationError(f'Operator "{operator}" takes exactly one "buildTime" argument.')
|
||||||
|
|
||||||
if operator in ('or', 'and') and seen_nonbool:
|
if operator in ('or', 'and') and seen_nonbool:
|
||||||
raise ValidationError('You cannot use OR/AND logic on a level below a comparison operator.')
|
raise ValidationError('You cannot use OR/AND logic on a level below a comparison operator.')
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ def _build_time(t=None, value=None, ev=None, now_dt=None):
|
|||||||
|
|
||||||
|
|
||||||
def _logic_annotate_for_graphic_explain(rules, ev, rule_data, now_dt):
|
def _logic_annotate_for_graphic_explain(rules, ev, rule_data, now_dt):
|
||||||
logic_environment = _get_logic_environment(ev, now_dt)
|
logic_environment = _get_logic_environment(ev, rule_data, now_dt)
|
||||||
event = ev if isinstance(ev, Event) else ev.event
|
event = ev if isinstance(ev, Event) else ev.event
|
||||||
|
|
||||||
def _evaluate_inners(r):
|
def _evaluate_inners(r):
|
||||||
@@ -112,6 +112,8 @@ def _logic_annotate_for_graphic_explain(rules, ev, rule_data, now_dt):
|
|||||||
val = str(event.items.get(pk=val))
|
val = str(event.items.get(pk=val))
|
||||||
elif var == "variation":
|
elif var == "variation":
|
||||||
val = str(ItemVariation.objects.get(item__event=event, pk=val))
|
val = str(ItemVariation.objects.get(item__event=event, pk=val))
|
||||||
|
elif var == "gate":
|
||||||
|
val = str(event.organizer.gates.get(pk=val))
|
||||||
elif isinstance(val, datetime):
|
elif isinstance(val, datetime):
|
||||||
val = date_format(val.astimezone(ev.timezone), "SHORT_DATETIME_FORMAT")
|
val = date_format(val.astimezone(ev.timezone), "SHORT_DATETIME_FORMAT")
|
||||||
return {"var": var, "__result": val}
|
return {"var": var, "__result": val}
|
||||||
@@ -152,7 +154,7 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
|
|||||||
get in before 17:00". In the middle of the night it would switch to "You can only get in after 09:00".
|
get in before 17:00". In the middle of the night it would switch to "You can only get in after 09:00".
|
||||||
"""
|
"""
|
||||||
now_dt = now_dt or now()
|
now_dt = now_dt or now()
|
||||||
logic_environment = _get_logic_environment(ev, now_dt)
|
logic_environment = _get_logic_environment(ev, rule_data, now_dt)
|
||||||
_var_values = {'False': False, 'True': True}
|
_var_values = {'False': False, 'True': True}
|
||||||
_var_explanations = {}
|
_var_explanations = {}
|
||||||
|
|
||||||
@@ -174,15 +176,22 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
|
|||||||
_var_values[new_var_name] = result
|
_var_values[new_var_name] = result
|
||||||
if not result:
|
if not result:
|
||||||
# Operator returned false, let's dig deeper
|
# Operator returned false, let's dig deeper
|
||||||
if "var" not in values[0]:
|
if "var" in values[0]:
|
||||||
|
if isinstance(values[0]["var"], list):
|
||||||
|
values[0]["var"] = values[0]["var"][0]
|
||||||
|
_var_explanations[new_var_name] = {
|
||||||
|
'operator': operator,
|
||||||
|
'var': values[0]["var"],
|
||||||
|
'rhs': values[1:],
|
||||||
|
}
|
||||||
|
elif "entries_since" in values[0] or "entries_before" in values[0]:
|
||||||
|
_var_explanations[new_var_name] = {
|
||||||
|
'operator': operator,
|
||||||
|
'var': values[0],
|
||||||
|
'rhs': values[1:],
|
||||||
|
}
|
||||||
|
else:
|
||||||
raise ValueError("Binary operators should be normalized to have a variable on their left-hand side")
|
raise ValueError("Binary operators should be normalized to have a variable on their left-hand side")
|
||||||
if isinstance(values[0]["var"], list):
|
|
||||||
values[0]["var"] = values[0]["var"][0]
|
|
||||||
_var_explanations[new_var_name] = {
|
|
||||||
'operator': operator,
|
|
||||||
'var': values[0]["var"],
|
|
||||||
'rhs': values[1:],
|
|
||||||
}
|
|
||||||
return {'var': new_var_name}
|
return {'var': new_var_name}
|
||||||
try:
|
try:
|
||||||
rules = _evaluate_inners(rules)
|
rules = _evaluate_inners(rules)
|
||||||
@@ -249,11 +258,17 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
|
|||||||
elif var == 'product' or var == 'variation':
|
elif var == 'product' or var == 'variation':
|
||||||
var_weights[vname] = (1000, 0)
|
var_weights[vname] = (1000, 0)
|
||||||
var_texts[vname] = _('Ticket type not allowed')
|
var_texts[vname] = _('Ticket type not allowed')
|
||||||
elif var in ('entries_number', 'entries_today', 'entries_days', 'minutes_since_last_entry', 'minutes_since_first_entry', 'now_isoweekday'):
|
elif var == 'gate':
|
||||||
|
var_weights[vname] = (10, 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)):
|
||||||
w = {
|
w = {
|
||||||
'minutes_since_first_entry': 80,
|
'minutes_since_first_entry': 80,
|
||||||
'minutes_since_last_entry': 90,
|
'minutes_since_last_entry': 90,
|
||||||
'entries_days': 100,
|
'entries_days': 100,
|
||||||
|
'entries_since': 110,
|
||||||
|
'entries_before': 110,
|
||||||
'entries_number': 120,
|
'entries_number': 120,
|
||||||
'entries_today': 140,
|
'entries_today': 140,
|
||||||
'now_isoweekday': 210,
|
'now_isoweekday': 210,
|
||||||
@@ -272,8 +287,24 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
|
|||||||
'entries_days': _('number of days with an entry'),
|
'entries_days': _('number of days with an entry'),
|
||||||
'entries_number': _('number of entries'),
|
'entries_number': _('number of entries'),
|
||||||
'entries_today': _('number of entries today'),
|
'entries_today': _('number of entries today'),
|
||||||
|
'entries_since': _('number of entries since {datetime}'),
|
||||||
|
'entries_before': _('number of entries before {datetime}'),
|
||||||
'now_isoweekday': _('week day'),
|
'now_isoweekday': _('week day'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isinstance(var, dict) and ("entries_since" in var or "entries_before" in var):
|
||||||
|
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):
|
||||||
|
compare_to_text = date_format(cutoff, 'TIME_FORMAT')
|
||||||
|
else:
|
||||||
|
compare_to_text = date_format(cutoff, 'SHORT_DATETIME_FORMAT')
|
||||||
|
l[varname] = str(l[varname].format(datetime=compare_to_text))
|
||||||
|
var = varname
|
||||||
|
var_result = getattr(rule_data, var)(cutoff)
|
||||||
|
else:
|
||||||
|
var_result = rule_data[var]
|
||||||
|
|
||||||
compare_to = rhs[0]
|
compare_to = rhs[0]
|
||||||
penalty = 0
|
penalty = 0
|
||||||
|
|
||||||
@@ -290,7 +321,7 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
|
|||||||
# These are "technical" comparisons without real meaning, we don't want to show them.
|
# These are "technical" comparisons without real meaning, we don't want to show them.
|
||||||
penalty = 1000
|
penalty = 1000
|
||||||
|
|
||||||
var_weights[vname] = (w[var] + operator_weights.get(operator, 0) + penalty, abs(compare_to - rule_data[var]))
|
var_weights[vname] = (w[var] + operator_weights.get(operator, 0) + penalty, abs(compare_to - var_result))
|
||||||
|
|
||||||
if var == 'now_isoweekday':
|
if var == 'now_isoweekday':
|
||||||
compare_to = {
|
compare_to = {
|
||||||
@@ -337,12 +368,14 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
|
|||||||
return ', '.join(var_texts[v] for v in paths_with_min_weight[0] if not _var_values[v])
|
return ', '.join(var_texts[v] for v in paths_with_min_weight[0] if not _var_values[v])
|
||||||
|
|
||||||
|
|
||||||
def _get_logic_environment(ev, now_dt):
|
def _get_logic_environment(ev, rule_data, now_dt):
|
||||||
# Every change to our supported JSON logic must be done
|
# Every change to our supported JSON logic must be done
|
||||||
# * in pretix.base.services.checkin
|
# * in pretix.base.services.checkin
|
||||||
# * in pretix.base.models.checkin
|
# * in pretix.base.models.checkin
|
||||||
|
# * in pretix.helpers.jsonlogic_boolalg
|
||||||
# * in checkinrules.js
|
# * in checkinrules.js
|
||||||
# * in libpretixsync
|
# * in libpretixsync
|
||||||
|
# * in pretixscan-ios
|
||||||
|
|
||||||
def is_before(t1, t2, tolerance=None):
|
def is_before(t1, t2, tolerance=None):
|
||||||
if tolerance:
|
if tolerance:
|
||||||
@@ -357,14 +390,18 @@ def _get_logic_environment(ev, now_dt):
|
|||||||
logic.add_operation('buildTime', partial(_build_time, ev=ev, now_dt=now_dt))
|
logic.add_operation('buildTime', partial(_build_time, ev=ev, now_dt=now_dt))
|
||||||
logic.add_operation('isBefore', is_before)
|
logic.add_operation('isBefore', is_before)
|
||||||
logic.add_operation('isAfter', lambda t1, t2, tol=None: is_before(t2, t1, tol))
|
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))
|
||||||
return logic
|
return logic
|
||||||
|
|
||||||
|
|
||||||
class LazyRuleVars:
|
class LazyRuleVars:
|
||||||
def __init__(self, position, clist, dt):
|
def __init__(self, position, clist, dt, gate):
|
||||||
self._position = position
|
self._position = position
|
||||||
self._clist = clist
|
self._clist = clist
|
||||||
self._dt = dt
|
self._dt = dt
|
||||||
|
self._gate = gate
|
||||||
|
self.__cache = {}
|
||||||
|
|
||||||
def __getitem__(self, item):
|
def __getitem__(self, item):
|
||||||
if item[0] != '_' and hasattr(self, item):
|
if item[0] != '_' and hasattr(self, item):
|
||||||
@@ -380,6 +417,10 @@ class LazyRuleVars:
|
|||||||
tz = self._clist.event.timezone
|
tz = self._clist.event.timezone
|
||||||
return self._dt.astimezone(tz).isoweekday()
|
return self._dt.astimezone(tz).isoweekday()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def gate(self):
|
||||||
|
return self._gate.pk if self._gate else None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def product(self):
|
def product(self):
|
||||||
return self._position.item_id
|
return self._position.item_id
|
||||||
@@ -398,6 +439,16 @@ class LazyRuleVars:
|
|||||||
midnight = self._dt.astimezone(tz).replace(hour=0, minute=0, second=0, microsecond=0)
|
midnight = self._dt.astimezone(tz).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
return self._position.checkins.filter(type=Checkin.TYPE_ENTRY, list=self._clist, datetime__gte=midnight).count()
|
return self._position.checkins.filter(type=Checkin.TYPE_ENTRY, list=self._clist, datetime__gte=midnight).count()
|
||||||
|
|
||||||
|
def entries_since(self, cutoff):
|
||||||
|
if ('entries_since', cutoff) not in self.__cache:
|
||||||
|
self.__cache['entries_since', cutoff] = self._position.checkins.filter(type=Checkin.TYPE_ENTRY, list=self._clist, datetime__gte=cutoff).count()
|
||||||
|
return self.__cache['entries_since', cutoff]
|
||||||
|
|
||||||
|
def entries_before(self, cutoff):
|
||||||
|
if ('entries_before', cutoff) not in self.__cache:
|
||||||
|
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]
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def entries_days(self):
|
def entries_days(self):
|
||||||
tz = self._clist.event.timezone
|
tz = self._clist.event.timezone
|
||||||
@@ -446,8 +497,9 @@ class SQLLogic:
|
|||||||
* Comparison operators (==, !=, …) never contain boolean operators (and, or) further down in the stack
|
* Comparison operators (==, !=, …) never contain boolean operators (and, or) further down in the stack
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, list):
|
def __init__(self, list, gate=None):
|
||||||
self.list = list
|
self.list = list
|
||||||
|
self.gate = gate
|
||||||
self.bool_ops = {
|
self.bool_ops = {
|
||||||
"and": lambda *args: reduce(lambda total, arg: total & arg, args) if args else Q(),
|
"and": lambda *args: reduce(lambda total, arg: total & arg, args) if args else Q(),
|
||||||
"or": lambda *args: reduce(lambda total, arg: total | arg, args) if args else Q(),
|
"or": lambda *args: reduce(lambda total, arg: total | arg, args) if args else Q(),
|
||||||
@@ -463,7 +515,7 @@ class SQLLogic:
|
|||||||
"isBefore": partial(self.comparison_to_q, operator=LowerThan, modifier=partial(tolerance, sign=1)),
|
"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)),
|
"isAfter": partial(self.comparison_to_q, operator=GreaterThan, modifier=partial(tolerance, sign=-1)),
|
||||||
}
|
}
|
||||||
self.expression_ops = {'buildTime', 'objectList', 'lookup', 'var'}
|
self.expression_ops = {'buildTime', 'objectList', 'lookup', 'var', 'entries_since', 'entries_before'}
|
||||||
|
|
||||||
def operation_to_expression(self, rule):
|
def operation_to_expression(self, rule):
|
||||||
if not isinstance(rule, dict):
|
if not isinstance(rule, dict):
|
||||||
@@ -511,6 +563,36 @@ class SQLLogic:
|
|||||||
return [self.operation_to_expression(v) for v in values]
|
return [self.operation_to_expression(v) for v in values]
|
||||||
elif operator == 'lookup':
|
elif operator == 'lookup':
|
||||||
return int(values[1])
|
return int(values[1])
|
||||||
|
elif operator == 'entries_since':
|
||||||
|
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]),
|
||||||
|
).values('position_id').order_by().annotate(
|
||||||
|
c=Count('*')
|
||||||
|
).values('c')
|
||||||
|
),
|
||||||
|
Value(0),
|
||||||
|
output_field=IntegerField()
|
||||||
|
)
|
||||||
|
elif operator == 'entries_before':
|
||||||
|
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]),
|
||||||
|
).values('position_id').order_by().annotate(
|
||||||
|
c=Count('*')
|
||||||
|
).values('c')
|
||||||
|
),
|
||||||
|
Value(0),
|
||||||
|
output_field=IntegerField()
|
||||||
|
)
|
||||||
elif operator == 'var':
|
elif operator == 'var':
|
||||||
if values[0] == 'now':
|
if values[0] == 'now':
|
||||||
return Value(now().astimezone(timezone.utc))
|
return Value(now().astimezone(timezone.utc))
|
||||||
@@ -520,6 +602,8 @@ class SQLLogic:
|
|||||||
return F('item_id')
|
return F('item_id')
|
||||||
elif values[0] == 'variation':
|
elif values[0] == 'variation':
|
||||||
return F('variation_id')
|
return F('variation_id')
|
||||||
|
elif values[0] == 'gate':
|
||||||
|
return Value(self.gate.pk if self.gate else None)
|
||||||
elif values[0] == 'entries_number':
|
elif values[0] == 'entries_number':
|
||||||
return Coalesce(
|
return Coalesce(
|
||||||
Subquery(
|
Subquery(
|
||||||
@@ -731,7 +815,8 @@ def _save_answers(op, answers, given_answers):
|
|||||||
def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict, force=False,
|
def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict, force=False,
|
||||||
ignore_unpaid=False, nonce=None, datetime=None, questions_supported=True,
|
ignore_unpaid=False, nonce=None, datetime=None, questions_supported=True,
|
||||||
user=None, auth=None, canceled_supported=False, type=Checkin.TYPE_ENTRY,
|
user=None, auth=None, canceled_supported=False, type=Checkin.TYPE_ENTRY,
|
||||||
raw_barcode=None, raw_source_type=None, from_revoked_secret=False, simulate=False):
|
raw_barcode=None, raw_source_type=None, from_revoked_secret=False, simulate=False,
|
||||||
|
gate=None):
|
||||||
"""
|
"""
|
||||||
Create a checkin for this particular order position and check-in list. Fails with CheckInError if the check in is
|
Create a checkin for this particular order position and check-in list. Fails with CheckInError if the check in is
|
||||||
not valid at this time.
|
not valid at this time.
|
||||||
@@ -746,6 +831,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
|||||||
:param nonce: A random nonce to prevent race conditions.
|
:param nonce: A random nonce to prevent race conditions.
|
||||||
:param datetime: The datetime of the checkin, defaults to now.
|
:param datetime: The datetime of the checkin, defaults to now.
|
||||||
:param simulate: If true, the check-in is not saved.
|
:param simulate: If true, the check-in is not saved.
|
||||||
|
:param gate: The gate the check-in was performed at.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# !!!!!!!!!
|
# !!!!!!!!!
|
||||||
@@ -860,8 +946,8 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
|||||||
)
|
)
|
||||||
|
|
||||||
if type == Checkin.TYPE_ENTRY and clist.rules:
|
if type == Checkin.TYPE_ENTRY and clist.rules:
|
||||||
rule_data = LazyRuleVars(op, clist, dt)
|
rule_data = LazyRuleVars(op, clist, dt, gate=gate)
|
||||||
logic = _get_logic_environment(op.subevent or clist.event, now_dt=dt)
|
logic = _get_logic_environment(op.subevent or clist.event, rule_data, now_dt=dt)
|
||||||
if not logic.apply(clist.rules, rule_data):
|
if not logic.apply(clist.rules, rule_data):
|
||||||
if force:
|
if force:
|
||||||
force_used = True
|
force_used = True
|
||||||
@@ -885,6 +971,8 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
|||||||
device = None
|
device = None
|
||||||
if isinstance(auth, Device):
|
if isinstance(auth, Device):
|
||||||
device = auth
|
device = auth
|
||||||
|
if not gate:
|
||||||
|
gate = device.gate
|
||||||
|
|
||||||
last_cis = list(op.checkins.order_by('-datetime').filter(list=clist).only('type', 'nonce', 'position_id'))
|
last_cis = list(op.checkins.order_by('-datetime').filter(list=clist).only('type', 'nonce', 'position_id'))
|
||||||
entry_allowed = (
|
entry_allowed = (
|
||||||
@@ -908,7 +996,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
|||||||
list=clist,
|
list=clist,
|
||||||
datetime=dt,
|
datetime=dt,
|
||||||
device=device,
|
device=device,
|
||||||
gate=device.gate if device else None,
|
gate=gate,
|
||||||
nonce=nonce,
|
nonce=nonce,
|
||||||
forced=force and (not entry_allowed or from_revoked_secret or force_used),
|
forced=force and (not entry_allowed or from_revoked_secret or force_used),
|
||||||
force_sent=force,
|
force_sent=force,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ from django_scopes.forms import (
|
|||||||
|
|
||||||
from pretix.base.channels import get_all_sales_channels
|
from pretix.base.channels import get_all_sales_channels
|
||||||
from pretix.base.forms.widgets import SplitDateTimePickerWidget
|
from pretix.base.forms.widgets import SplitDateTimePickerWidget
|
||||||
|
from pretix.base.models import Gate
|
||||||
from pretix.base.models.checkin import Checkin, CheckinList
|
from pretix.base.models.checkin import Checkin, CheckinList
|
||||||
from pretix.control.forms import ItemMultipleChoiceField
|
from pretix.control.forms import ItemMultipleChoiceField
|
||||||
from pretix.control.forms.widgets import Select2
|
from pretix.control.forms.widgets import Select2
|
||||||
@@ -201,3 +202,26 @@ class CheckinListSimulatorForm(forms.Form):
|
|||||||
initial=True,
|
initial=True,
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
|
gate = SafeModelChoiceField(
|
||||||
|
label=_('Gate'),
|
||||||
|
empty_label=_('All gates'),
|
||||||
|
queryset=Gate.objects.none(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.event = kwargs.pop('event')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.fields['gate'].queryset = self.event.organizer.gates.all()
|
||||||
|
self.fields['gate'].widget = Select2(
|
||||||
|
attrs={
|
||||||
|
'data-model-select2': 'generic',
|
||||||
|
'data-select2-url': reverse('control:organizer.gates.select2', kwargs={
|
||||||
|
'organizer': self.event.organizer.slug,
|
||||||
|
}),
|
||||||
|
'data-placeholder': _('All gates'),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.fields['gate'].widget.choices = self.fields['gate'].choices
|
||||||
|
self.fields['gate'].label = _('Gate')
|
||||||
|
|||||||
@@ -29,6 +29,8 @@
|
|||||||
id="product-select2">{% url "control:event.items.select2" event=request.event.slug organizer=request.organizer.slug %}</script>
|
id="product-select2">{% url "control:event.items.select2" event=request.event.slug organizer=request.organizer.slug %}</script>
|
||||||
<script type="text/plain"
|
<script type="text/plain"
|
||||||
id="variations-select2">{% url "control:event.items.variations.select2" event=request.event.slug organizer=request.organizer.slug %}</script>
|
id="variations-select2">{% url "control:event.items.variations.select2" event=request.event.slug organizer=request.organizer.slug %}</script>
|
||||||
|
<script type="text/plain"
|
||||||
|
id="gates-select2">{% url "control:organizer.gates.select2" organizer=request.organizer.slug %}</script>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% bootstrap_form_errors form %}
|
{% bootstrap_form_errors form %}
|
||||||
<div class="tabbed-form">
|
<div class="tabbed-form">
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
{% bootstrap_field form.raw_barcode layout="control" %}
|
{% bootstrap_field form.raw_barcode layout="control" %}
|
||||||
{% bootstrap_field form.datetime layout="control" %}
|
{% bootstrap_field form.datetime layout="control" %}
|
||||||
{% bootstrap_field form.checkin_type layout="control" %}
|
{% bootstrap_field form.checkin_type layout="control" %}
|
||||||
|
{% bootstrap_field form.gate layout="control" %}
|
||||||
{% bootstrap_field form.ignore_unpaid layout="control" %}
|
{% bootstrap_field form.ignore_unpaid layout="control" %}
|
||||||
{% bootstrap_field form.questions_supported layout="control" %}
|
{% bootstrap_field form.questions_supported layout="control" %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|||||||
@@ -495,6 +495,11 @@ class CheckInListSimulator(EventPermissionRequiredMixin, FormView):
|
|||||||
r['Content-Security-Policy'] = 'script-src \'unsafe-eval\''
|
r['Content-Security-Policy'] = 'script-src \'unsafe-eval\''
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
kwargs = super().get_form_kwargs()
|
||||||
|
kwargs['event'] = self.request.event
|
||||||
|
return kwargs
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
return {
|
return {
|
||||||
'datetime': now()
|
'datetime': now()
|
||||||
@@ -528,12 +533,13 @@ class CheckInListSimulator(EventPermissionRequiredMixin, FormView):
|
|||||||
request=self.request, # this is not clean, but we need it in the serializers for URL generation
|
request=self.request, # this is not clean, but we need it in the serializers for URL generation
|
||||||
legacy_url_support=False,
|
legacy_url_support=False,
|
||||||
simulate=True,
|
simulate=True,
|
||||||
|
gate=form.cleaned_data.get("gate"),
|
||||||
).data
|
).data
|
||||||
|
|
||||||
if form.cleaned_data["checkin_type"] == Checkin.TYPE_ENTRY and self.list.rules and self.result.get("position")\
|
if form.cleaned_data["checkin_type"] == Checkin.TYPE_ENTRY and self.list.rules and self.result.get("position")\
|
||||||
and (self.result["status"] in ("ok", "incomplete") or self.result["reason"] == "rules"):
|
and (self.result["status"] in ("ok", "incomplete") or self.result["reason"] == "rules"):
|
||||||
op = OrderPosition.objects.get(pk=self.result["position"]["id"])
|
op = OrderPosition.objects.get(pk=self.result["position"]["id"])
|
||||||
rule_data = LazyRuleVars(op, self.list, form.cleaned_data["datetime"])
|
rule_data = LazyRuleVars(op, self.list, form.cleaned_data["datetime"], form.cleaned_data.get("gate"))
|
||||||
rule_graph = _logic_annotate_for_graphic_explain(self.list.rules, op.subevent or self.list.event, rule_data,
|
rule_graph = _logic_annotate_for_graphic_explain(self.list.rules, op.subevent or self.list.event, rule_data,
|
||||||
form.cleaned_data["datetime"])
|
form.cleaned_data["datetime"])
|
||||||
self.result["rule_graph"] = rule_graph
|
self.result["rule_graph"] = rule_graph
|
||||||
|
|||||||
@@ -1010,7 +1010,7 @@ def devices_select2(request, **kwargs):
|
|||||||
return JsonResponse(doc)
|
return JsonResponse(doc)
|
||||||
|
|
||||||
|
|
||||||
@organizer_permission_required(("can_view_orders", "can_change_organizer_settings"))
|
@organizer_permission_required(("can_view_orders", "can_change_event_settings", "can_change_organizer_settings"))
|
||||||
# This decorator is a bit of a hack since this is not technically an organizer permission, but it does the job here --
|
# This decorator is a bit of a hack since this is not technically an organizer permission, but it does the job here --
|
||||||
# anyone who can see orders for any event can see the check-in log view where this is used as a filter
|
# anyone who can see orders for any event can see the check-in log view where this is used as a filter
|
||||||
def gate_select2(request, **kwargs):
|
def gate_select2(request, **kwargs):
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ $(function () {
|
|||||||
// Every change to our supported JSON logic must be done
|
// Every change to our supported JSON logic must be done
|
||||||
// * in pretix.base.services.checkin
|
// * in pretix.base.services.checkin
|
||||||
// * in pretix.base.models.checkin
|
// * in pretix.base.models.checkin
|
||||||
|
// * in pretix.helpers.jsonlogic_boolalg
|
||||||
// * in checkinrules.js
|
// * in checkinrules.js
|
||||||
// * in libpretixsync
|
// * in libpretixsync
|
||||||
|
// * in pretixscan-ios
|
||||||
'product': {
|
'product': {
|
||||||
'inList': {
|
'inList': {
|
||||||
'label': gettext('is one of'),
|
'label': gettext('is one of'),
|
||||||
@@ -17,6 +19,12 @@ $(function () {
|
|||||||
'cardinality': 2,
|
'cardinality': 2,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
'gate': {
|
||||||
|
'inList': {
|
||||||
|
'label': gettext('is one of'),
|
||||||
|
'cardinality': 2,
|
||||||
|
}
|
||||||
|
},
|
||||||
'datetime': {
|
'datetime': {
|
||||||
'isBefore': {
|
'isBefore': {
|
||||||
'label': gettext('is before'),
|
'label': gettext('is before'),
|
||||||
@@ -27,6 +35,32 @@ $(function () {
|
|||||||
'cardinality': 2,
|
'cardinality': 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'int_by_datetime': {
|
||||||
|
'<': {
|
||||||
|
'label': '<',
|
||||||
|
'cardinality': 2,
|
||||||
|
},
|
||||||
|
'<=': {
|
||||||
|
'label': '≤',
|
||||||
|
'cardinality': 2,
|
||||||
|
},
|
||||||
|
'>': {
|
||||||
|
'label': '>',
|
||||||
|
'cardinality': 2,
|
||||||
|
},
|
||||||
|
'>=': {
|
||||||
|
'label': '≥',
|
||||||
|
'cardinality': 2,
|
||||||
|
},
|
||||||
|
'==': {
|
||||||
|
'label': '=',
|
||||||
|
'cardinality': 2,
|
||||||
|
},
|
||||||
|
'!=': {
|
||||||
|
'label': '≠',
|
||||||
|
'cardinality': 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
'int': {
|
'int': {
|
||||||
'<': {
|
'<': {
|
||||||
'label': '<',
|
'label': '<',
|
||||||
@@ -63,6 +97,10 @@ $(function () {
|
|||||||
'label': gettext('Product variation'),
|
'label': gettext('Product variation'),
|
||||||
'type': 'variation',
|
'type': 'variation',
|
||||||
},
|
},
|
||||||
|
'gate': {
|
||||||
|
'label': gettext('Gate'),
|
||||||
|
'type': 'gate',
|
||||||
|
},
|
||||||
'now': {
|
'now': {
|
||||||
'label': gettext('Current date and time'),
|
'label': gettext('Current date and time'),
|
||||||
'type': 'datetime',
|
'type': 'datetime',
|
||||||
@@ -79,6 +117,14 @@ $(function () {
|
|||||||
'label': gettext('Number of previous entries since midnight'),
|
'label': gettext('Number of previous entries since midnight'),
|
||||||
'type': 'int',
|
'type': 'int',
|
||||||
},
|
},
|
||||||
|
'entries_since': {
|
||||||
|
'label': gettext('Number of previous entries since'),
|
||||||
|
'type': 'int_by_datetime',
|
||||||
|
},
|
||||||
|
'entries_before': {
|
||||||
|
'label': gettext('Number of previous entries before'),
|
||||||
|
'type': 'int_by_datetime',
|
||||||
|
},
|
||||||
'entries_days': {
|
'entries_days': {
|
||||||
'label': gettext('Number of days with a previous entry'),
|
'label': gettext('Number of days with a previous entry'),
|
||||||
'type': 'int',
|
'type': 'int',
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<button type="button" class="checkin-rule-remove btn btn-xs btn-default" @click.prevent="wrapWithAND">AND
|
<button type="button" class="checkin-rule-remove btn btn-xs btn-default" @click.prevent="wrapWithAND">AND
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="checkin-rule-remove btn btn-xs btn-default" @click.prevent="cutOut"
|
<button type="button" class="checkin-rule-remove btn btn-xs btn-default" @click.prevent="cutOut"
|
||||||
v-if="operands && operands.length == 1 && (operator === 'or' || operator == 'and')"><span
|
v-if="operands && operands.length === 1 && (operator === 'or' || operator === 'and')"><span
|
||||||
class="fa fa-cut"></span></button>
|
class="fa fa-cut"></span></button>
|
||||||
<button type="button" class="checkin-rule-remove btn btn-xs btn-default" @click.prevent="remove"
|
<button type="button" class="checkin-rule-remove btn btn-xs btn-default" @click.prevent="remove"
|
||||||
v-if="level > 0"><span class="fa fa-trash"></span></button>
|
v-if="level > 0"><span class="fa fa-trash"></span></button>
|
||||||
@@ -21,33 +21,41 @@
|
|||||||
<option v-for="(v, name) in vars" :value="name">{{ v.label }}</option>
|
<option v-for="(v, name) in vars" :value="name">{{ v.label }}</option>
|
||||||
</select>
|
</select>
|
||||||
<select v-bind:value="operator" v-on:input="setOperator" required class="form-control"
|
<select v-bind:value="operator" v-on:input="setOperator" required class="form-control"
|
||||||
v-if="operator !== 'or' && operator !== 'and'">
|
v-if="operator !== 'or' && operator !== 'and' && vartype !== 'int_by_datetime'">
|
||||||
<option></option>
|
<option></option>
|
||||||
<option v-for="(v, name) in operators" :value="name">{{ v.label }}</option>
|
<option v-for="(v, name) in operators" :value="name">{{ v.label }}</option>
|
||||||
</select>
|
</select>
|
||||||
<select v-bind:value="timeType" v-on:input="setTimeType" required class="form-control"
|
<select v-bind:value="timeType" v-on:input="setTimeType" required class="form-control"
|
||||||
v-if="vartype == 'datetime'">
|
v-if="vartype === 'datetime' || vartype === 'int_by_datetime'">
|
||||||
<option value="date_from">{{texts.date_from}}</option>
|
<option value="date_from">{{texts.date_from}}</option>
|
||||||
<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>
|
<option value="customtime">{{texts.date_customtime}}</option>
|
||||||
</select>
|
</select>
|
||||||
<datetimefield v-if="vartype == 'datetime' && timeType == 'custom'" :value="timeValue"
|
<datetimefield v-if="(vartype === 'datetime' || vartype === 'int_by_datetime') && timeType === 'custom'" :value="timeValue"
|
||||||
v-on:input="setTimeValue"></datetimefield>
|
v-on:input="setTimeValue"></datetimefield>
|
||||||
<timefield v-if="vartype == 'datetime' && timeType == 'customtime'" :value="timeValue"
|
<timefield v-if="(vartype === 'datetime' || vartype === 'int_by_datetime') && timeType === 'customtime'" :value="timeValue"
|
||||||
v-on:input="setTimeValue"></timefield>
|
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 != 'customtime' && 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"
|
<select v-bind:value="operator" v-on:input="setOperator" required class="form-control"
|
||||||
|
v-if="vartype === 'int_by_datetime'">
|
||||||
|
<option></option>
|
||||||
|
<option v-for="(v, name) in operators" :value="name">{{ v.label }}</option>
|
||||||
|
</select>
|
||||||
|
<input class="form-control" required type="number" v-if="(vartype === 'int' || vartype === 'int_by_datetime') && cardinality > 1"
|
||||||
v-bind:value="rightoperand" v-on:input="setRightOperandNumber">
|
v-bind:value="rightoperand" v-on:input="setRightOperandNumber">
|
||||||
<lookup-select2 required v-if="vartype == 'product' && operator == 'inList'" :multiple="true"
|
<lookup-select2 required v-if="vartype === 'product' && operator === 'inList'" :multiple="true"
|
||||||
:value="rightoperand" v-on:input="setRightOperandProductList"
|
:value="rightoperand" v-on:input="setRightOperandProductList"
|
||||||
:url="productSelectURL"></lookup-select2>
|
:url="productSelectURL"></lookup-select2>
|
||||||
<lookup-select2 required v-if="vartype == 'variation' && operator == 'inList'" :multiple="true"
|
<lookup-select2 required v-if="vartype === 'variation' && operator === 'inList'" :multiple="true"
|
||||||
:value="rightoperand" v-on:input="setRightOperandVariationList"
|
:value="rightoperand" v-on:input="setRightOperandVariationList"
|
||||||
:url="variationSelectURL"></lookup-select2>
|
:url="variationSelectURL"></lookup-select2>
|
||||||
|
<lookup-select2 required v-if="vartype === 'gate' && operator === 'inList'" :multiple="true"
|
||||||
|
:value="rightoperand" v-on:input="setRightOperandGateList"
|
||||||
|
:url="gateSelectURL"></lookup-select2>
|
||||||
<div class="checkin-rule-childrules" v-if="operator === 'or' || operator === 'and'">
|
<div class="checkin-rule-childrules" v-if="operator === 'or' || operator === 'and'">
|
||||||
<div v-for="(op, opi) in operands">
|
<div v-for="(op, opi) in operands">
|
||||||
<checkin-rule :rule="op" :index="opi" :level="level + 1" v-if="typeof op === 'object'"></checkin-rule>
|
<checkin-rule :rule="op" :index="opi" :level="level + 1" v-if="typeof op === 'object'"></checkin-rule>
|
||||||
@@ -79,6 +87,12 @@
|
|||||||
if (op === "and" || op === "or") {
|
if (op === "and" || op === "or") {
|
||||||
return op;
|
return op;
|
||||||
} else if (this.rule[op] && this.rule[op][0]) {
|
} else if (this.rule[op] && this.rule[op][0]) {
|
||||||
|
if (this.rule[op][0]["entries_since"]) {
|
||||||
|
return "entries_since";
|
||||||
|
}
|
||||||
|
if (this.rule[op][0]["entries_before"]) {
|
||||||
|
return "entries_before";
|
||||||
|
}
|
||||||
return this.rule[op][0]["var"];
|
return this.rule[op][0]["var"];
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
@@ -113,7 +127,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
timeType: function () {
|
timeType: function () {
|
||||||
if (this.rightoperand && this.rightoperand['buildTime']) {
|
if (this.vartype === 'int_by_datetime') {
|
||||||
|
if (this.rule[this.operator][0][this.variable] && this.rule[this.operator][0][this.variable][0]['buildTime']) {
|
||||||
|
return this.rule[this.operator][0][this.variable][0]['buildTime'][0];
|
||||||
|
}
|
||||||
|
} else if (this.rightoperand && this.rightoperand['buildTime']) {
|
||||||
return this.rightoperand['buildTime'][0];
|
return this.rightoperand['buildTime'][0];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -126,7 +144,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
timeValue: function () {
|
timeValue: function () {
|
||||||
if (this.rightoperand && this.rightoperand['buildTime']) {
|
if (this.vartype === 'int_by_datetime') {
|
||||||
|
if (this.rule[this.operator][0][this.variable][0]['buildTime']) {
|
||||||
|
return this.rule[this.operator][0][this.variable][0]['buildTime'][1];
|
||||||
|
}
|
||||||
|
} else if (this.rightoperand && this.rightoperand['buildTime']) {
|
||||||
return this.rightoperand['buildTime'][1];
|
return this.rightoperand['buildTime'][1];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -144,6 +166,9 @@
|
|||||||
variationSelectURL: function () {
|
variationSelectURL: function () {
|
||||||
return $("#variations-select2").text();
|
return $("#variations-select2").text();
|
||||||
},
|
},
|
||||||
|
gateSelectURL: function () {
|
||||||
|
return $("#gates-select2").text();
|
||||||
|
},
|
||||||
vars: function () {
|
vars: function () {
|
||||||
return this.$root.VARS;
|
return this.$root.VARS;
|
||||||
},
|
},
|
||||||
@@ -161,7 +186,19 @@
|
|||||||
this.$delete(this.rule, current_op);
|
this.$delete(this.rule, current_op);
|
||||||
} else {
|
} else {
|
||||||
if (current_val !== "and" && current_val !== "or" && current_val[0] && this.$root.VARS[event.target.value]['type'] === this.vartype) {
|
if (current_val !== "and" && current_val !== "or" && current_val[0] && this.$root.VARS[event.target.value]['type'] === this.vartype) {
|
||||||
this.$set(this.rule[current_op][0], "var", event.target.value);
|
if (this.vartype === "int_by_datetime") {
|
||||||
|
var current_data = this.rule[current_op][0][this.variable];
|
||||||
|
var new_lhs = {};
|
||||||
|
new_lhs[event.target.value] = JSON.parse(JSON.stringify(current_data));
|
||||||
|
this.$set(this.rule[current_op], 0, new_lhs);
|
||||||
|
} else {
|
||||||
|
this.$set(this.rule[current_op][0], "var", event.target.value);
|
||||||
|
}
|
||||||
|
} else if (this.$root.VARS[event.target.value]['type'] === 'int_by_datetime') {
|
||||||
|
this.$delete(this.rule, current_op);
|
||||||
|
var o = {};
|
||||||
|
o[event.target.value] = [{"buildTime": [null, null]}]
|
||||||
|
this.$set(this.rule, "!!", [o]);
|
||||||
} else {
|
} else {
|
||||||
this.$delete(this.rule, current_op);
|
this.$delete(this.rule, current_op);
|
||||||
this.$set(this.rule, "!!", [{"var": event.target.value}]);
|
this.$set(this.rule, "!!", [{"var": event.target.value}]);
|
||||||
@@ -192,17 +229,25 @@
|
|||||||
var time = {
|
var time = {
|
||||||
"buildTime": [event.target.value]
|
"buildTime": [event.target.value]
|
||||||
};
|
};
|
||||||
if (this.rule[this.operator].length === 1) {
|
if (this.vartype === "int_by_datetime") {
|
||||||
this.rule[this.operator].push(time);
|
this.$set(this.rule[this.operator][0][this.variable], 0, time);
|
||||||
} else {
|
} else {
|
||||||
this.$set(this.rule[this.operator], 1, time);
|
if (this.rule[this.operator].length === 1) {
|
||||||
}
|
this.rule[this.operator].push(time);
|
||||||
if (event.target.value === "custom") {
|
} else {
|
||||||
this.$set(this.rule[this.operator], 2, 0);
|
this.$set(this.rule[this.operator], 1, time);
|
||||||
|
}
|
||||||
|
if (event.target.value === "custom") {
|
||||||
|
this.$set(this.rule[this.operator], 2, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setTimeValue: function (val) {
|
setTimeValue: function (val) {
|
||||||
this.$set(this.rule[this.operator][1]["buildTime"], 1, val);
|
if (this.vartype === "int_by_datetime") {
|
||||||
|
this.$set(this.rule[this.operator][0][this.variable][0]["buildTime"], 1, val);
|
||||||
|
} else {
|
||||||
|
this.$set(this.rule[this.operator][1]["buildTime"], 1, val);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
setRightOperandProductList: function (val) {
|
setRightOperandProductList: function (val) {
|
||||||
var products = {
|
var products = {
|
||||||
@@ -242,6 +287,25 @@
|
|||||||
this.$set(this.rule[this.operator], 1, products);
|
this.$set(this.rule[this.operator], 1, products);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setRightOperandGateList: function (val) {
|
||||||
|
var products = {
|
||||||
|
"objectList": []
|
||||||
|
};
|
||||||
|
for (var i = 0; i < val.length; i++) {
|
||||||
|
products["objectList"].push({
|
||||||
|
"lookup": [
|
||||||
|
"gate",
|
||||||
|
val[i].id,
|
||||||
|
val[i].text
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (this.rule[this.operator].length === 1) {
|
||||||
|
this.rule[this.operator].push(products);
|
||||||
|
} else {
|
||||||
|
this.$set(this.rule[this.operator], 1, products);
|
||||||
|
}
|
||||||
|
},
|
||||||
addOperand: function () {
|
addOperand: function () {
|
||||||
this.rule[this.operator].push({"": []});
|
this.rule[this.operator].push({"": []});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,6 +19,26 @@
|
|||||||
{{ op.label }} {{ rightoperand }}
|
{{ op.label }} {{ rightoperand }}
|
||||||
</strong>
|
</strong>
|
||||||
</span>
|
</span>
|
||||||
|
<span v-else-if="vardata && vardata.type === 'int_by_datetime'">
|
||||||
|
<span v-if="variable.startsWith('entries_')" class="fa fa-sign-in"></span>
|
||||||
|
{{ vardata.label }}
|
||||||
|
<span v-if="node.rule[operator][0][variable][0].buildTime[0] === 'custom'">
|
||||||
|
{{ df(node.rule[operator][0][variable][0].buildTime[1]) }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="node.rule[operator][0][variable][0].buildTime[0] === 'customtime'">
|
||||||
|
{{ tf(node.rule[operator][0][variable][0].buildTime[1]) }}
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
{{ this.$root.texts[node.rule[operator][0][variable][0].buildTime[0]] }}
|
||||||
|
</span>
|
||||||
|
<br>
|
||||||
|
<span v-if="varresult !== null">
|
||||||
|
{{varresult}}
|
||||||
|
</span>
|
||||||
|
<strong>
|
||||||
|
{{ op.label }} {{ rightoperand }}
|
||||||
|
</strong>
|
||||||
|
</span>
|
||||||
<span v-else-if="vardata && variable === 'now'">
|
<span v-else-if="vardata && variable === 'now'">
|
||||||
<span class="fa fa-clock-o"></span> {{ vardata.label }}<br>
|
<span class="fa fa-clock-o"></span> {{ vardata.label }}<br>
|
||||||
<span v-if="varresult !== null">
|
<span v-if="varresult !== null">
|
||||||
@@ -44,7 +64,9 @@
|
|||||||
</strong>
|
</strong>
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="vardata && operator === 'inList'">
|
<span v-else-if="vardata && operator === 'inList'">
|
||||||
<span class="fa fa-ticket"></span> {{ vardata.label }}
|
<span class="fa fa-sign-in" v-if="variable === 'gate'"></span>
|
||||||
|
<span class="fa fa-ticket" v-else></span>
|
||||||
|
{{ vardata.label }}
|
||||||
<span v-if="varresult !== null">
|
<span v-if="varresult !== null">
|
||||||
({{varresult}})
|
({{varresult}})
|
||||||
</span>
|
</span>
|
||||||
@@ -138,6 +160,12 @@
|
|||||||
variable () {
|
variable () {
|
||||||
const op = this.operator;
|
const op = this.operator;
|
||||||
if (this.node.rule[op] && this.node.rule[op][0]) {
|
if (this.node.rule[op] && this.node.rule[op][0]) {
|
||||||
|
if (this.node.rule[op][0]["entries_since"]) {
|
||||||
|
return "entries_since";
|
||||||
|
}
|
||||||
|
if (this.node.rule[op][0]["entries_before"]) {
|
||||||
|
return "entries_before";
|
||||||
|
}
|
||||||
return this.node.rule[op][0]["var"];
|
return this.node.rule[op][0]["var"];
|
||||||
} else {
|
} else {
|
||||||
return "";
|
return "";
|
||||||
@@ -149,6 +177,8 @@
|
|||||||
varresult () {
|
varresult () {
|
||||||
const op = this.operator;
|
const op = this.operator;
|
||||||
if (this.node.rule[op] && this.node.rule[op][0]) {
|
if (this.node.rule[op] && this.node.rule[op][0]) {
|
||||||
|
if (typeof this.node.rule[op][0]["__result"] === "undefined")
|
||||||
|
return null;
|
||||||
return this.node.rule[op][0]["__result"];
|
return this.node.rule[op][0]["__result"];
|
||||||
} else {
|
} else {
|
||||||
return "";
|
return "";
|
||||||
|
|||||||
@@ -562,6 +562,44 @@ def test_rules_variation(item, position, clist):
|
|||||||
perform_checkin(position, clist, {})
|
perform_checkin(position, clist, {})
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_rules_gate(event, item, position, clist):
|
||||||
|
g1 = event.organizer.gates.create(name="Gate 1")
|
||||||
|
g2 = event.organizer.gates.create(name="Gate 2")
|
||||||
|
clist.rules = {
|
||||||
|
"inList": [
|
||||||
|
{"var": "gate"}, {
|
||||||
|
"objectList": [
|
||||||
|
{"lookup": ["gate", str(g1.pk), "Gate 1"]},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
clist.save()
|
||||||
|
with pytest.raises(CheckInError):
|
||||||
|
perform_checkin(position, clist, {}, gate=None)
|
||||||
|
with pytest.raises(CheckInError) as excinfo:
|
||||||
|
perform_checkin(position, clist, {}, gate=g2)
|
||||||
|
assert not OrderPosition.objects.filter(SQLLogic(clist, gate=g2).apply(clist.rules), pk=position.pk).exists()
|
||||||
|
assert not OrderPosition.objects.filter(SQLLogic(clist, gate=None).apply(clist.rules), pk=position.pk).exists()
|
||||||
|
assert excinfo.value.code == 'rules'
|
||||||
|
assert 'Wrong entrance gate' in str(excinfo.value)
|
||||||
|
|
||||||
|
clist.rules = {
|
||||||
|
"inList": [
|
||||||
|
{"var": "gate"}, {
|
||||||
|
"objectList": [
|
||||||
|
{"lookup": ["gate", str(g1.pk), "Gate 1"]},
|
||||||
|
{"lookup": ["gate", str(g2.pk), "Gate 2"]},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
clist.save()
|
||||||
|
assert OrderPosition.objects.filter(SQLLogic(clist, gate=g2).apply(clist.rules), pk=position.pk).exists()
|
||||||
|
perform_checkin(position, clist, {}, gate=g2)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_rules_scan_number(position, clist):
|
def test_rules_scan_number(position, clist):
|
||||||
# Ticket is valid three times
|
# Ticket is valid three times
|
||||||
@@ -708,6 +746,106 @@ def test_rules_scan_days(event, position, clist):
|
|||||||
assert 'Maximum number of days with an entry exceeded.' in str(excinfo.value)
|
assert 'Maximum number of days with an entry exceeded.' in str(excinfo.value)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_rules_entries_since(event, position, clist):
|
||||||
|
# Ticket is valid once before X and once 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]},
|
||||||
|
{"<=": [{"entries_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-01 23:10: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 since 23:00 exceeded' in str(excinfo.value)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_rules_entries_since_time_of_day(event, position, clist):
|
||||||
|
# Ticket is valid daily once before X and once after X
|
||||||
|
event.settings.timezone = 'Europe/Berlin'
|
||||||
|
clist.allow_multiple_entries = True
|
||||||
|
clist.rules = {
|
||||||
|
"or": [
|
||||||
|
{"<=": [{"var": "entries_today"}, 0]},
|
||||||
|
{"and": [
|
||||||
|
{"isAfter": [{"var": "now"}, {"buildTime": ["customtime", "23:00:00"]}, 0]},
|
||||||
|
{"<=": [{"entries_since": [{"buildTime": ["customtime", "23:00:00"]}]}, 0]},
|
||||||
|
]},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
clist.save()
|
||||||
|
|
||||||
|
for t in ["2020-01-01 22:00:00+01:00", "2020-01-01 23:01:00+01:00", "2020-01-02 22:00:00+01:00", "2020-01-02 23:01:00+01:00"]:
|
||||||
|
with freeze_time(t):
|
||||||
|
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'
|
||||||
|
if now().astimezone(event.timezone).hour < 23:
|
||||||
|
assert 'Maximum number of entries today exceeded' in str(excinfo.value)
|
||||||
|
else:
|
||||||
|
assert 'Maximum number of entries since 23:00 exceeded' in str(excinfo.value)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_rules_entries_before(event, position, clist):
|
||||||
|
# Ticket is valid after 23:00 only if people already showed up 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_before": [{"buildTime": ["custom", "2020-01-01T23:00:00.000+01:00"]}]}, 1]},
|
||||||
|
]},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
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, {})
|
||||||
|
|
||||||
|
with freeze_time("2020-01-01 23:10:00+01:00"):
|
||||||
|
assert OrderPosition.objects.filter(SQLLogic(clist).apply(clist.rules), pk=position.pk).exists()
|
||||||
|
perform_checkin(position, clist, {})
|
||||||
|
|
||||||
|
position.all_checkins.all().delete()
|
||||||
|
|
||||||
|
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 entries before 23:00 exceeded' in str(excinfo.value)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_rules_time_isafter_tolerance(event, position, clist):
|
def test_rules_time_isafter_tolerance(event, position, clist):
|
||||||
# Ticket is valid starting 10 minutes before admission time
|
# Ticket is valid starting 10 minutes before admission time
|
||||||
|
|||||||
Reference in New Issue
Block a user