mirror of
https://github.com/pretix/pretix.git
synced 2026-08-18 12:06:26 +00:00
Add check-in simulator (#3380)
This commit is contained in:
@@ -396,7 +396,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):
|
source_type='barcode', legacy_url_support=False, simulate=False):
|
||||||
if not checkinlists:
|
if not checkinlists:
|
||||||
raise ValidationError('No check-in list passed.')
|
raise ValidationError('No check-in list passed.')
|
||||||
|
|
||||||
@@ -433,6 +433,8 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
|||||||
)
|
)
|
||||||
raw_barcode_for_checkin = None
|
raw_barcode_for_checkin = None
|
||||||
from_revoked_secret = False
|
from_revoked_secret = False
|
||||||
|
if simulate:
|
||||||
|
common_checkin_args['__fake_arg_to_prevent_this_from_being_saved'] = True
|
||||||
|
|
||||||
# 1. Gather a list of positions that could be the one we looking for, either from their ID, secret or
|
# 1. Gather a list of positions that could be the one we looking for, either from their ID, secret or
|
||||||
# parent secret
|
# parent secret
|
||||||
@@ -472,13 +474,14 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
|||||||
revoked_matches = list(
|
revoked_matches = list(
|
||||||
RevokedTicketSecret.objects.filter(event_id__in=list_by_event.keys(), secret=raw_barcode))
|
RevokedTicketSecret.objects.filter(event_id__in=list_by_event.keys(), secret=raw_barcode))
|
||||||
if len(revoked_matches) == 0:
|
if len(revoked_matches) == 0:
|
||||||
checkinlists[0].event.log_action('pretix.event.checkin.unknown', data={
|
if not simulate:
|
||||||
'datetime': datetime,
|
checkinlists[0].event.log_action('pretix.event.checkin.unknown', data={
|
||||||
'type': checkin_type,
|
'datetime': datetime,
|
||||||
'list': checkinlists[0].pk,
|
'type': checkin_type,
|
||||||
'barcode': raw_barcode,
|
'list': checkinlists[0].pk,
|
||||||
'searched_lists': [cl.pk for cl in checkinlists]
|
'barcode': raw_barcode,
|
||||||
}, user=user, auth=auth)
|
'searched_lists': [cl.pk for cl in checkinlists]
|
||||||
|
}, user=user, auth=auth)
|
||||||
|
|
||||||
for cl in checkinlists:
|
for cl in checkinlists:
|
||||||
for k, s in cl.event.ticket_secret_generators.items():
|
for k, s in cl.event.ticket_secret_generators.items():
|
||||||
@@ -492,12 +495,13 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
Checkin.objects.create(
|
if not simulate:
|
||||||
position=None,
|
Checkin.objects.create(
|
||||||
successful=False,
|
position=None,
|
||||||
error_reason=Checkin.REASON_INVALID,
|
successful=False,
|
||||||
**common_checkin_args,
|
error_reason=Checkin.REASON_INVALID,
|
||||||
)
|
**common_checkin_args,
|
||||||
|
)
|
||||||
|
|
||||||
if force and legacy_url_support and isinstance(auth, Device):
|
if force and legacy_url_support and isinstance(auth, Device):
|
||||||
# There was a bug in libpretixsync: If you scanned a ticket in offline mode that was
|
# There was a bug in libpretixsync: If you scanned a ticket in offline mode that was
|
||||||
@@ -539,19 +543,20 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
|||||||
from_revoked_secret = True
|
from_revoked_secret = True
|
||||||
else:
|
else:
|
||||||
op = revoked_matches[0].position
|
op = revoked_matches[0].position
|
||||||
op.order.log_action('pretix.event.checkin.revoked', data={
|
if not simulate:
|
||||||
'datetime': datetime,
|
op.order.log_action('pretix.event.checkin.revoked', data={
|
||||||
'type': checkin_type,
|
'datetime': datetime,
|
||||||
'list': list_by_event[revoked_matches[0].event_id].pk,
|
'type': checkin_type,
|
||||||
'barcode': raw_barcode
|
'list': list_by_event[revoked_matches[0].event_id].pk,
|
||||||
}, user=user, auth=auth)
|
'barcode': raw_barcode
|
||||||
common_checkin_args['list'] = list_by_event[revoked_matches[0].event_id]
|
}, user=user, auth=auth)
|
||||||
Checkin.objects.create(
|
common_checkin_args['list'] = list_by_event[revoked_matches[0].event_id]
|
||||||
position=op,
|
Checkin.objects.create(
|
||||||
successful=False,
|
position=op,
|
||||||
error_reason=Checkin.REASON_REVOKED,
|
successful=False,
|
||||||
**common_checkin_args
|
error_reason=Checkin.REASON_REVOKED,
|
||||||
)
|
**common_checkin_args
|
||||||
|
)
|
||||||
return Response({
|
return Response({
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
'reason': Checkin.REASON_REVOKED,
|
'reason': Checkin.REASON_REVOKED,
|
||||||
@@ -588,24 +593,25 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
|||||||
# We choose the first match (regardless of product) for the logging since it's most likely to be the
|
# We choose the first match (regardless of product) for the logging since it's most likely to be the
|
||||||
# base product according to our order_by above.
|
# base product according to our order_by above.
|
||||||
op = op_candidates[0]
|
op = op_candidates[0]
|
||||||
op.order.log_action('pretix.event.checkin.denied', data={
|
if not simulate:
|
||||||
'position': op.id,
|
op.order.log_action('pretix.event.checkin.denied', data={
|
||||||
'positionid': op.positionid,
|
'position': op.id,
|
||||||
'errorcode': Checkin.REASON_AMBIGUOUS,
|
'positionid': op.positionid,
|
||||||
'reason_explanation': None,
|
'errorcode': Checkin.REASON_AMBIGUOUS,
|
||||||
'force': force,
|
'reason_explanation': None,
|
||||||
'datetime': datetime,
|
'force': force,
|
||||||
'type': checkin_type,
|
'datetime': datetime,
|
||||||
'list': list_by_event[op.order.event_id].pk,
|
'type': checkin_type,
|
||||||
}, user=user, auth=auth)
|
'list': list_by_event[op.order.event_id].pk,
|
||||||
common_checkin_args['list'] = list_by_event[op.order.event_id]
|
}, user=user, auth=auth)
|
||||||
Checkin.objects.create(
|
common_checkin_args['list'] = list_by_event[op.order.event_id]
|
||||||
position=op,
|
Checkin.objects.create(
|
||||||
successful=False,
|
position=op,
|
||||||
error_reason=Checkin.REASON_AMBIGUOUS,
|
successful=False,
|
||||||
error_explanation=None,
|
error_reason=Checkin.REASON_AMBIGUOUS,
|
||||||
**common_checkin_args,
|
error_explanation=None,
|
||||||
)
|
**common_checkin_args,
|
||||||
|
)
|
||||||
return Response({
|
return Response({
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
'reason': Checkin.REASON_AMBIGUOUS,
|
'reason': Checkin.REASON_AMBIGUOUS,
|
||||||
@@ -652,6 +658,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
|||||||
raw_barcode=raw_barcode_for_checkin,
|
raw_barcode=raw_barcode_for_checkin,
|
||||||
raw_source_type=source_type,
|
raw_source_type=source_type,
|
||||||
from_revoked_secret=from_revoked_secret,
|
from_revoked_secret=from_revoked_secret,
|
||||||
|
simulate=simulate,
|
||||||
)
|
)
|
||||||
except RequiredQuestionsError as e:
|
except RequiredQuestionsError as e:
|
||||||
return Response({
|
return Response({
|
||||||
@@ -664,23 +671,24 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
|||||||
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
|
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
|
||||||
}, status=400)
|
}, status=400)
|
||||||
except CheckInError as e:
|
except CheckInError as e:
|
||||||
op.order.log_action('pretix.event.checkin.denied', data={
|
if not simulate:
|
||||||
'position': op.id,
|
op.order.log_action('pretix.event.checkin.denied', data={
|
||||||
'positionid': op.positionid,
|
'position': op.id,
|
||||||
'errorcode': e.code,
|
'positionid': op.positionid,
|
||||||
'reason_explanation': e.reason,
|
'errorcode': e.code,
|
||||||
'force': force,
|
'reason_explanation': e.reason,
|
||||||
'datetime': datetime,
|
'force': force,
|
||||||
'type': checkin_type,
|
'datetime': datetime,
|
||||||
'list': list_by_event[op.order.event_id].pk,
|
'type': checkin_type,
|
||||||
}, user=user, auth=auth)
|
'list': list_by_event[op.order.event_id].pk,
|
||||||
Checkin.objects.create(
|
}, user=user, auth=auth)
|
||||||
position=op,
|
Checkin.objects.create(
|
||||||
successful=False,
|
position=op,
|
||||||
error_reason=e.code,
|
successful=False,
|
||||||
error_explanation=e.reason,
|
error_reason=e.code,
|
||||||
**common_checkin_args,
|
error_explanation=e.reason,
|
||||||
)
|
**common_checkin_args,
|
||||||
|
)
|
||||||
return Response({
|
return Response({
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
'reason': e.code,
|
'reason': e.code,
|
||||||
|
|||||||
@@ -53,7 +53,8 @@ from django.utils.translation import gettext as _
|
|||||||
from django_scopes import scope, scopes_disabled
|
from django_scopes import scope, scopes_disabled
|
||||||
|
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Checkin, CheckinList, Device, Order, OrderPosition, QuestionOption,
|
Checkin, CheckinList, Device, Event, ItemVariation, Order, OrderPosition,
|
||||||
|
QuestionOption,
|
||||||
)
|
)
|
||||||
from pretix.base.signals import checkin_created, order_placed, periodic_task
|
from pretix.base.signals import checkin_created, order_placed, periodic_task
|
||||||
from pretix.helpers import OF_SELF
|
from pretix.helpers import OF_SELF
|
||||||
@@ -65,12 +66,13 @@ from pretix.helpers.jsonlogic_query import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _build_time(t=None, value=None, ev=None):
|
def _build_time(t=None, value=None, ev=None, now_dt=None):
|
||||||
|
now_dt = now_dt or now()
|
||||||
if t == "custom":
|
if t == "custom":
|
||||||
return dateutil.parser.parse(value)
|
return dateutil.parser.parse(value)
|
||||||
elif t == "customtime":
|
elif t == "customtime":
|
||||||
parsed = dateutil.parser.parse(value)
|
parsed = dateutil.parser.parse(value)
|
||||||
return now().astimezone(ev.timezone).replace(
|
return now_dt.astimezone(ev.timezone).replace(
|
||||||
hour=parsed.hour,
|
hour=parsed.hour,
|
||||||
minute=parsed.minute,
|
minute=parsed.minute,
|
||||||
second=parsed.second,
|
second=parsed.second,
|
||||||
@@ -84,7 +86,42 @@ def _build_time(t=None, value=None, ev=None):
|
|||||||
return ev.date_admission or ev.date_from
|
return ev.date_admission or ev.date_from
|
||||||
|
|
||||||
|
|
||||||
def _logic_explain(rules, ev, rule_data):
|
def _logic_annotate_for_graphic_explain(rules, ev, rule_data):
|
||||||
|
logic_environment = _get_logic_environment(ev)
|
||||||
|
event = ev if isinstance(ev, Event) else ev.event
|
||||||
|
|
||||||
|
def _evaluate_inners(r):
|
||||||
|
if not isinstance(r, dict):
|
||||||
|
return r
|
||||||
|
operator = list(r.keys())[0]
|
||||||
|
values = r[operator]
|
||||||
|
if operator in ("and", "or"):
|
||||||
|
return {operator: [_evaluate_inners(v) for v in values]}
|
||||||
|
result = logic_environment.apply(r, rule_data)
|
||||||
|
return {**r, '__result': result}
|
||||||
|
|
||||||
|
def _add_var_values(r):
|
||||||
|
if not isinstance(r, dict):
|
||||||
|
return r
|
||||||
|
operator = [k for k in r.keys() if not k.startswith("__")][0]
|
||||||
|
values = r[operator]
|
||||||
|
if operator == "var":
|
||||||
|
var = values[0] if isinstance(values, list) else values
|
||||||
|
val = rule_data[var]
|
||||||
|
if var == "product":
|
||||||
|
val = str(event.items.get(pk=val))
|
||||||
|
elif var == "variation":
|
||||||
|
val = str(ItemVariation.objects.get(item__event=event, pk=val))
|
||||||
|
elif isinstance(val, datetime):
|
||||||
|
val = date_format(val.astimezone(ev.timezone), "SHORT_DATETIME_FORMAT")
|
||||||
|
return {"var": var, "__result": val}
|
||||||
|
else:
|
||||||
|
return {**r, operator: [_add_var_values(v) for v in values]}
|
||||||
|
|
||||||
|
return _add_var_values(_evaluate_inners(rules))
|
||||||
|
|
||||||
|
|
||||||
|
def _logic_explain(rules, ev, rule_data, now_dt=None):
|
||||||
"""
|
"""
|
||||||
Explains when the logic denied the check-in. Only works for a denied check-in.
|
Explains when the logic denied the check-in. Only works for a denied check-in.
|
||||||
|
|
||||||
@@ -114,6 +151,7 @@ def _logic_explain(rules, ev, rule_data):
|
|||||||
Additionally, we favor a "close failure". Therefore, in the above example, we'd show "You can only
|
Additionally, we favor a "close failure". Therefore, in the above example, we'd show "You can only
|
||||||
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()
|
||||||
logic_environment = _get_logic_environment(ev)
|
logic_environment = _get_logic_environment(ev)
|
||||||
_var_values = {'False': False, 'True': True}
|
_var_values = {'False': False, 'True': True}
|
||||||
_var_explanations = {}
|
_var_explanations = {}
|
||||||
@@ -198,9 +236,9 @@ def _logic_explain(rules, ev, rule_data):
|
|||||||
else:
|
else:
|
||||||
compare_to -= tolerance
|
compare_to -= tolerance
|
||||||
|
|
||||||
var_weights[vname] = (200, abs(now() - compare_to).total_seconds())
|
var_weights[vname] = (200, abs(now_dt - compare_to).total_seconds())
|
||||||
|
|
||||||
if abs(now() - compare_to) < timedelta(hours=12):
|
if abs(now_dt - compare_to) < timedelta(hours=12):
|
||||||
compare_to_text = date_format(compare_to, 'TIME_FORMAT')
|
compare_to_text = date_format(compare_to, 'TIME_FORMAT')
|
||||||
else:
|
else:
|
||||||
compare_to_text = date_format(compare_to, 'SHORT_DATETIME_FORMAT')
|
compare_to_text = date_format(compare_to, 'SHORT_DATETIME_FORMAT')
|
||||||
@@ -357,7 +395,7 @@ class LazyRuleVars:
|
|||||||
@cached_property
|
@cached_property
|
||||||
def entries_today(self):
|
def entries_today(self):
|
||||||
tz = self._clist.event.timezone
|
tz = self._clist.event.timezone
|
||||||
midnight = now().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()
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
@@ -378,7 +416,7 @@ class LazyRuleVars:
|
|||||||
# between platforms (None<1 is true on some, but not all), we rather choose something that is at least
|
# between platforms (None<1 is true on some, but not all), we rather choose something that is at least
|
||||||
# consistent.
|
# consistent.
|
||||||
return -1
|
return -1
|
||||||
return (now() - last_entry.datetime).total_seconds() // 60
|
return (self._dt - last_entry.datetime).total_seconds() // 60
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def minutes_since_first_entry(self):
|
def minutes_since_first_entry(self):
|
||||||
@@ -390,7 +428,7 @@ class LazyRuleVars:
|
|||||||
# between platforms (None<1 is true on some, but not all), we rather choose something that is at least
|
# between platforms (None<1 is true on some, but not all), we rather choose something that is at least
|
||||||
# consistent.
|
# consistent.
|
||||||
return -1
|
return -1
|
||||||
return (now() - last_entry.datetime).total_seconds() // 60
|
return (self._dt - last_entry.datetime).total_seconds() // 60
|
||||||
|
|
||||||
|
|
||||||
class SQLLogic:
|
class SQLLogic:
|
||||||
@@ -693,7 +731,7 @@ 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):
|
raw_barcode=None, raw_source_type=None, from_revoked_secret=False, simulate=False):
|
||||||
"""
|
"""
|
||||||
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.
|
||||||
@@ -707,6 +745,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
|||||||
:param questions_supported: When set to False, questions are ignored
|
:param questions_supported: When set to False, questions are ignored
|
||||||
: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.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# !!!!!!!!!
|
# !!!!!!!!!
|
||||||
@@ -734,7 +773,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
|||||||
'blocked'
|
'blocked'
|
||||||
)
|
)
|
||||||
|
|
||||||
if type != Checkin.TYPE_EXIT and op.valid_from and op.valid_from > now():
|
if type != Checkin.TYPE_EXIT and op.valid_from and op.valid_from > dt:
|
||||||
if force:
|
if force:
|
||||||
force_used = True
|
force_used = True
|
||||||
else:
|
else:
|
||||||
@@ -748,7 +787,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if type != Checkin.TYPE_EXIT and op.valid_until and op.valid_until < now():
|
if type != Checkin.TYPE_EXIT and op.valid_until and op.valid_until < dt:
|
||||||
if force:
|
if force:
|
||||||
force_used = True
|
force_used = True
|
||||||
else:
|
else:
|
||||||
@@ -773,7 +812,8 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
|||||||
if q not in given_answers and q not in answers:
|
if q not in given_answers and q not in answers:
|
||||||
require_answers.append(q)
|
require_answers.append(q)
|
||||||
|
|
||||||
_save_answers(op, answers, given_answers)
|
if not simulate:
|
||||||
|
_save_answers(op, answers, given_answers)
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
# Lock order positions, if it is an entry. We don't need it for exits, as a race condition wouldn't be problematic
|
# Lock order positions, if it is an entry. We don't need it for exits, as a race condition wouldn't be problematic
|
||||||
@@ -859,30 +899,33 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
|||||||
return
|
return
|
||||||
|
|
||||||
if entry_allowed or force:
|
if entry_allowed or force:
|
||||||
ci = Checkin.objects.create(
|
if simulate:
|
||||||
position=op,
|
return True
|
||||||
type=type,
|
else:
|
||||||
list=clist,
|
ci = Checkin.objects.create(
|
||||||
datetime=dt,
|
position=op,
|
||||||
device=device,
|
type=type,
|
||||||
gate=device.gate if device else None,
|
list=clist,
|
||||||
nonce=nonce,
|
datetime=dt,
|
||||||
forced=force and (not entry_allowed or from_revoked_secret or force_used),
|
device=device,
|
||||||
force_sent=force,
|
gate=device.gate if device else None,
|
||||||
raw_barcode=raw_barcode,
|
nonce=nonce,
|
||||||
raw_source_type=raw_source_type,
|
forced=force and (not entry_allowed or from_revoked_secret or force_used),
|
||||||
)
|
force_sent=force,
|
||||||
op.order.log_action('pretix.event.checkin', data={
|
raw_barcode=raw_barcode,
|
||||||
'position': op.id,
|
raw_source_type=raw_source_type,
|
||||||
'positionid': op.positionid,
|
)
|
||||||
'first': True,
|
op.order.log_action('pretix.event.checkin', data={
|
||||||
'forced': force or op.order.status != Order.STATUS_PAID,
|
'position': op.id,
|
||||||
'datetime': dt,
|
'positionid': op.positionid,
|
||||||
'type': type,
|
'first': True,
|
||||||
'answers': {k.pk: str(v) for k, v in given_answers.items()},
|
'forced': force or op.order.status != Order.STATUS_PAID,
|
||||||
'list': clist.pk
|
'datetime': dt,
|
||||||
}, user=user, auth=auth)
|
'type': type,
|
||||||
checkin_created.send(op.order.event, checkin=ci)
|
'answers': {k.pk: str(v) for k, v in given_answers.items()},
|
||||||
|
'list': clist.pk
|
||||||
|
}, user=user, auth=auth)
|
||||||
|
checkin_created.send(op.order.event, checkin=ci)
|
||||||
else:
|
else:
|
||||||
raise CheckInError(
|
raise CheckInError(
|
||||||
_('This ticket has already been redeemed.'),
|
_('This ticket has already been redeemed.'),
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ 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.models.checkin import CheckinList
|
from pretix.base.forms.widgets import SplitDateTimePickerWidget
|
||||||
|
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
|
||||||
|
|
||||||
@@ -177,3 +178,26 @@ class SimpleCheckinListForm(forms.ModelForm):
|
|||||||
'subevent': SafeModelChoiceField,
|
'subevent': SafeModelChoiceField,
|
||||||
'gates': SafeModelMultipleChoiceField,
|
'gates': SafeModelMultipleChoiceField,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CheckinListSimulatorForm(forms.Form):
|
||||||
|
raw_barcode = forms.CharField(
|
||||||
|
label=_("Barcode"),
|
||||||
|
)
|
||||||
|
datetime = forms.SplitDateTimeField(
|
||||||
|
label=_("Check-in time"),
|
||||||
|
widget=SplitDateTimePickerWidget(),
|
||||||
|
)
|
||||||
|
checkin_type = forms.ChoiceField(
|
||||||
|
label=_("Check-in type"),
|
||||||
|
choices=Checkin.CHECKIN_TYPES,
|
||||||
|
)
|
||||||
|
ignore_unpaid = forms.BooleanField(
|
||||||
|
label=_("Allow check-in of unpaid order (if check-in list permits it)"),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
questions_supported = forms.BooleanField(
|
||||||
|
label=_("Support for check-in questions"),
|
||||||
|
initial=True,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|||||||
@@ -16,6 +16,11 @@
|
|||||||
{% trans "Edit list configuration" %}
|
{% trans "Edit list configuration" %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<a href="{% url "control:event.orders.checkinlists.simulator" event=request.event.slug organizer=request.event.organizer.slug list=checkinlist.pk %}"
|
||||||
|
class="btn btn-default">
|
||||||
|
<span class="fa fa-flask"></span>
|
||||||
|
{% trans "Check-in simulator" %}
|
||||||
|
</a>
|
||||||
<a href="{% url "control:event.orders.export" event=request.event.slug organizer=request.event.organizer.slug %}?identifier=checkinlistpdf&checkinlistpdf-list={{ checkinlist.pk }}"
|
<a href="{% url "control:event.orders.export" event=request.event.slug organizer=request.event.organizer.slug %}?identifier=checkinlistpdf&checkinlistpdf-list={{ checkinlist.pk }}"
|
||||||
class="btn btn-default" target="_blank">
|
class="btn btn-default" target="_blank">
|
||||||
<span class="fa fa-download"></span>
|
<span class="fa fa-download"></span>
|
||||||
|
|||||||
@@ -12,7 +12,15 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block inside %}
|
{% block inside %}
|
||||||
{% if checkinlist %}
|
{% if checkinlist %}
|
||||||
<h1>{% blocktrans with name=checkinlist.name %}Check-in list: {{ name }}{% endblocktrans %}</h1>
|
<h1>
|
||||||
|
{% blocktrans with name=checkinlist.name %}Check-in list: {{ name }}{% endblocktrans %}
|
||||||
|
<a href="{% url "control:event.orders.checkinlists.simulator" event=request.event.slug organizer=request.event.organizer.slug list=checkinlist.pk %}"
|
||||||
|
target="_blank"
|
||||||
|
class="btn btn-default">
|
||||||
|
<span class="fa fa-flask"></span>
|
||||||
|
{% trans "Check-in simulator" %}
|
||||||
|
</a>
|
||||||
|
</h1>
|
||||||
{% else %}
|
{% else %}
|
||||||
<h1>{% trans "Check-in list" %}</h1>
|
<h1>{% trans "Check-in list" %}</h1>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -133,6 +133,9 @@
|
|||||||
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
|
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
|
||||||
<span class="fa fa-copy"></span>
|
<span class="fa fa-copy"></span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{% url "control:event.orders.checkinlists.simulator" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}"
|
||||||
|
title="{% trans "Check-in simulator" %}" data-toggle="tooltip"
|
||||||
|
class="btn btn-default btn-sm"><i class="fa fa-flask"></i></a>
|
||||||
<a href="{% url "control:event.orders.checkinlists.edit" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}" class="btn btn-default btn-sm"><i class="fa fa-wrench"></i></a>
|
<a href="{% url "control:event.orders.checkinlists.edit" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}" class="btn btn-default btn-sm"><i class="fa fa-wrench"></i></a>
|
||||||
<a href="{% url "control:event.orders.checkinlists.delete" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
<a href="{% url "control:event.orders.checkinlists.delete" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
{% extends "pretixcontrol/items/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
{% load escapejson %}
|
||||||
|
{% load getitem %}
|
||||||
|
{% load static %}
|
||||||
|
{% load compress %}
|
||||||
|
{% block title %}{% trans "Check-in simulator" %}{% endblock %}
|
||||||
|
{% block inside %}
|
||||||
|
<h1>
|
||||||
|
{% blocktrans with name=checkinlist.name %}Check-in list: {{ name }}{% endblocktrans %}
|
||||||
|
{% if 'can_change_event_settings' in request.eventpermset %}
|
||||||
|
<a href="{% url "control:event.orders.checkinlists.edit" event=request.event.slug organizer=request.event.organizer.slug list=checkinlist.pk %}"
|
||||||
|
class="btn btn-default">
|
||||||
|
<span class="fa fa-wrench"></span>
|
||||||
|
{% trans "Edit list configuration" %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</h1>
|
||||||
|
<h2>{% trans "Check-in simulator" %}</h2>
|
||||||
|
<p>
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
This tool allows you to validate your check-in configuration. You can enter a barcode plus some
|
||||||
|
optional parameters and we will show you the response of the check-in list. No actual check-in will
|
||||||
|
be performed and no modification to the system state is made.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
<form action="" method="post" class="form-horizontal">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% bootstrap_form_errors form %}
|
||||||
|
{% bootstrap_field form.raw_barcode layout="control" %}
|
||||||
|
{% bootstrap_field form.datetime layout="control" %}
|
||||||
|
{% bootstrap_field form.checkin_type layout="control" %}
|
||||||
|
{% bootstrap_field form.ignore_unpaid layout="control" %}
|
||||||
|
{% bootstrap_field form.questions_supported layout="control" %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-9 col-md-offset-3">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
{% trans "Simulate" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% if result %}
|
||||||
|
<hr>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<div class="panel-title">{% trans "Result" %}</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body checkin-sim-result checkin-sim-result-status-{{ result.status }} checkin-sim-result-reason-{{ result.reason }}">
|
||||||
|
{% if result.status == "ok" %}
|
||||||
|
<span class="fa fa-check-circle"></span>
|
||||||
|
{% elif result.status == "incomplete" %}
|
||||||
|
<span class="fa fa-question-circle"></span>
|
||||||
|
{% elif result.status == "error" %}
|
||||||
|
{% if result.reason == "already_redeemed" %}
|
||||||
|
<span class="fa fa-warning"></span>
|
||||||
|
{% else %}
|
||||||
|
<span class="fa fa-exclamation-circle"></span>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% if result.status == "ok" %}
|
||||||
|
<h3 class="nomargin-top">{% trans "Valid check-in" %}</h3>
|
||||||
|
{% elif result.status == "incomplete" %}
|
||||||
|
<h3 class="nomargin-top">{% trans "Additional information required" %}</h3>
|
||||||
|
<p>
|
||||||
|
{% trans "The following questions must be answered before check-in can be completed:" %}
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
{% for q in result.questions %}
|
||||||
|
<li>
|
||||||
|
<a href="{% url "control:event.items.questions.show" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}">
|
||||||
|
{{ q.question }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% elif result.status == "error" %}
|
||||||
|
<h3 class="nomargin-top">{{ reason_labels|getitem:result.reason }}</h3>
|
||||||
|
{% if result.reason_explanation %}
|
||||||
|
<p>{{ result.reason_explanation }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% if result.position %}
|
||||||
|
{% if result.position.require_attention %}
|
||||||
|
<p>
|
||||||
|
<span class="fa fa-info-circle fa-fw"></span> {% trans "Special attention required" %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
<p>
|
||||||
|
<span class="fa fa-ticket fa-fw"></span>
|
||||||
|
<a href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=result.position.order %}">
|
||||||
|
{{ result.position.order }}</a>-{{ result.position.positionid }}
|
||||||
|
</p>
|
||||||
|
{% if result.position.attendee_name %}
|
||||||
|
<p>
|
||||||
|
<span class="fa fa-user fa-fw"></span>
|
||||||
|
{{ result.position.attendee_name }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% if result.rule_graph %}
|
||||||
|
<div id="rules-editor" class="form-inline">
|
||||||
|
<div role="tabpanel" class="tab-pane" id="rules-viz">
|
||||||
|
<checkin-rules-visualization></checkin-rules-visualization>
|
||||||
|
</div>
|
||||||
|
<textarea id="id_rules" class="sr-only">{{ result.rule_graph|attr_escapejson_dumps }}</textarea>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
{% if DEBUG %}
|
||||||
|
<script type="text/javascript" src="{% static "vuejs/vue.js" %}"></script>
|
||||||
|
{% else %}
|
||||||
|
<script type="text/javascript" src="{% static "vuejs/vue.min.js" %}"></script>
|
||||||
|
{% endif %}
|
||||||
|
{% compress js %}
|
||||||
|
<script type="text/javascript" src="{% static "d3/d3.v6.js" %}"></script>
|
||||||
|
<script type="text/javascript" src="{% static "d3/d3-color.v2.js" %}"></script>
|
||||||
|
<script type="text/javascript" src="{% static "d3/d3-dispatch.v2.js" %}"></script>
|
||||||
|
<script type="text/javascript" src="{% static "d3/d3-ease.v2.js" %}"></script>
|
||||||
|
<script type="text/javascript" src="{% static "d3/d3-interpolate.v2.js" %}"></script>
|
||||||
|
<script type="text/javascript" src="{% static "d3/d3-selection.v2.js" %}"></script>
|
||||||
|
<script type="text/javascript" src="{% static "d3/d3-timer.v2.js" %}"></script>
|
||||||
|
<script type="text/javascript" src="{% static "d3/d3-transition.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>
|
||||||
|
{% endcompress %}
|
||||||
|
{% compress js %}
|
||||||
|
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/checkinrules/jsonlogic-boolalg.js" %}"></script>
|
||||||
|
<script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/viz-node.vue' %}"></script>
|
||||||
|
<script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/checkin-rules-visualization.vue' %}"></script>
|
||||||
|
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/checkinrules.js" %}"></script>
|
||||||
|
{% endcompress %}
|
||||||
|
{% endblock %}
|
||||||
@@ -429,6 +429,7 @@ urlpatterns = [
|
|||||||
re_path(r'^checkinlists/add$', checkin.CheckinListCreate.as_view(), name='event.orders.checkinlists.add'),
|
re_path(r'^checkinlists/add$', checkin.CheckinListCreate.as_view(), name='event.orders.checkinlists.add'),
|
||||||
re_path(r'^checkinlists/select2$', typeahead.checkinlist_select2, name='event.orders.checkinlists.select2'),
|
re_path(r'^checkinlists/select2$', typeahead.checkinlist_select2, name='event.orders.checkinlists.select2'),
|
||||||
re_path(r'^checkinlists/(?P<list>\d+)/$', checkin.CheckInListShow.as_view(), name='event.orders.checkinlists.show'),
|
re_path(r'^checkinlists/(?P<list>\d+)/$', checkin.CheckInListShow.as_view(), name='event.orders.checkinlists.show'),
|
||||||
|
re_path(r'^checkinlists/(?P<list>\d+)/simulator$', checkin.CheckInListSimulator.as_view(), name='event.orders.checkinlists.simulator'),
|
||||||
re_path(r'^checkinlists/(?P<list>\d+)/bulk_action$', checkin.CheckInListBulkActionView.as_view(), name='event.orders.checkinlists.bulk_action'),
|
re_path(r'^checkinlists/(?P<list>\d+)/bulk_action$', checkin.CheckInListBulkActionView.as_view(), name='event.orders.checkinlists.bulk_action'),
|
||||||
re_path(r'^checkinlists/(?P<list>\d+)/change$', checkin.CheckinListUpdate.as_view(),
|
re_path(r'^checkinlists/(?P<list>\d+)/change$', checkin.CheckinListUpdate.as_view(),
|
||||||
name='event.orders.checkinlists.edit'),
|
name='event.orders.checkinlists.edit'),
|
||||||
|
|||||||
@@ -19,6 +19,19 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
|
||||||
|
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||||
|
#
|
||||||
|
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
|
||||||
|
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
|
||||||
|
#
|
||||||
|
# This file contains Apache-licensed contributions copyrighted by: Jakob Schnell, jasonwaiting@live.hk, pajowu
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||||
|
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
|
import secrets
|
||||||
from datetime import timezone
|
from datetime import timezone
|
||||||
|
|
||||||
import dateutil.parser
|
import dateutil.parser
|
||||||
@@ -32,14 +45,21 @@ from django.urls import reverse
|
|||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.timezone import is_aware, make_aware, now
|
from django.utils.timezone import is_aware, make_aware, now
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import ListView
|
from django.views.generic import FormView, ListView
|
||||||
|
from i18nfield.strings import LazyI18nString
|
||||||
|
|
||||||
|
from pretix.api.views.checkin import _redeem_process
|
||||||
from pretix.base.channels import get_all_sales_channels
|
from pretix.base.channels import get_all_sales_channels
|
||||||
from pretix.base.models import Checkin, Order, OrderPosition
|
from pretix.base.models import Checkin, Order, OrderPosition
|
||||||
from pretix.base.models.checkin import CheckinList
|
from pretix.base.models.checkin import CheckinList
|
||||||
|
from pretix.base.services.checkin import (
|
||||||
|
LazyRuleVars, _logic_annotate_for_graphic_explain,
|
||||||
|
)
|
||||||
from pretix.base.signals import checkin_created
|
from pretix.base.signals import checkin_created
|
||||||
from pretix.base.views.tasks import AsyncPostView
|
from pretix.base.views.tasks import AsyncPostView
|
||||||
from pretix.control.forms.checkin import CheckinListForm
|
from pretix.control.forms.checkin import (
|
||||||
|
CheckinListForm, CheckinListSimulatorForm,
|
||||||
|
)
|
||||||
from pretix.control.forms.filter import (
|
from pretix.control.forms.filter import (
|
||||||
CheckinFilterForm, CheckinListAttendeeFilterForm,
|
CheckinFilterForm, CheckinListAttendeeFilterForm,
|
||||||
)
|
)
|
||||||
@@ -48,18 +68,6 @@ from pretix.control.views import CreateView, PaginationMixin, UpdateView
|
|||||||
from pretix.helpers.compat import CompatDeleteView
|
from pretix.helpers.compat import CompatDeleteView
|
||||||
from pretix.helpers.models import modelcopy
|
from pretix.helpers.models import modelcopy
|
||||||
|
|
||||||
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
|
|
||||||
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
|
|
||||||
#
|
|
||||||
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
|
|
||||||
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
|
|
||||||
#
|
|
||||||
# This file contains Apache-licensed contributions copyrighted by: Jakob Schnell, jasonwaiting@live.hk, pajowu
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
|
||||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations under the License.
|
|
||||||
|
|
||||||
|
|
||||||
class CheckInListQueryMixin:
|
class CheckInListQueryMixin:
|
||||||
|
|
||||||
@@ -469,3 +477,63 @@ class CheckinListView(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
|||||||
ctx = super().get_context_data()
|
ctx = super().get_context_data()
|
||||||
ctx['filter_form'] = self.filter_form
|
ctx['filter_form'] = self.filter_form
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
class CheckInListSimulator(EventPermissionRequiredMixin, FormView):
|
||||||
|
template_name = 'pretixcontrol/checkin/simulator.html'
|
||||||
|
permission = 'can_view_orders'
|
||||||
|
form_class = CheckinListSimulatorForm
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
self.list = get_object_or_404(self.request.event.checkin_lists.all(), pk=kwargs.get("list"))
|
||||||
|
self.result = None
|
||||||
|
r = super().dispatch(request, *args, **kwargs)
|
||||||
|
r['Content-Security-Policy'] = 'script-src \'unsafe-eval\''
|
||||||
|
return r
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
return {
|
||||||
|
'datetime': now()
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
return super().get_context_data(
|
||||||
|
**kwargs,
|
||||||
|
checkinlist=self.list,
|
||||||
|
result=self.result,
|
||||||
|
reason_labels=dict(Checkin.REASONS),
|
||||||
|
)
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
self.result = _redeem_process(
|
||||||
|
checkinlists=[self.list],
|
||||||
|
raw_barcode=form.cleaned_data["raw_barcode"],
|
||||||
|
answers_data={},
|
||||||
|
datetime=form.cleaned_data["datetime"],
|
||||||
|
force=False,
|
||||||
|
checkin_type=form.cleaned_data["checkin_type"],
|
||||||
|
ignore_unpaid=form.cleaned_data["ignore_unpaid"],
|
||||||
|
untrusted_input=True,
|
||||||
|
user=self.request.user,
|
||||||
|
auth=None,
|
||||||
|
expand=[],
|
||||||
|
nonce=secrets.token_hex(12),
|
||||||
|
pdf_data=False,
|
||||||
|
questions_supported=form.cleaned_data["questions_supported"],
|
||||||
|
canceled_supported=False,
|
||||||
|
request=self.request, # this is not clean, but we need it in the serializers for URL generation
|
||||||
|
legacy_url_support=False,
|
||||||
|
simulate=True,
|
||||||
|
).data
|
||||||
|
|
||||||
|
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"):
|
||||||
|
op = OrderPosition.objects.get(pk=self.result["position"]["id"])
|
||||||
|
rule_data = LazyRuleVars(op, self.list, form.cleaned_data["datetime"])
|
||||||
|
rule_graph = _logic_annotate_for_graphic_explain(self.list.rules, op.subevent or self.list.event, rule_data)
|
||||||
|
self.result["rule_graph"] = rule_graph
|
||||||
|
|
||||||
|
if self.result.get("questions"):
|
||||||
|
for q in self.result["questions"]:
|
||||||
|
q["question"] = LazyI18nString(q["question"])
|
||||||
|
return self.get(self.request, self.args, self.kwargs)
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ class Logic():
|
|||||||
|
|
||||||
data = data or {}
|
data = data or {}
|
||||||
|
|
||||||
operator = list(tests.keys())[0]
|
operator = [k for k in tests.keys() if not k.startswith("__")][0]
|
||||||
values = tests[operator]
|
values = tests[operator]
|
||||||
|
|
||||||
# Easy syntax for unary operators, like {"var": "x"} instead of strict
|
# Easy syntax for unary operators, like {"var": "x"} instead of strict
|
||||||
|
|||||||
@@ -93,13 +93,19 @@ $(function () {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Vue.component('checkin-rule', CheckinRule.default);
|
var components = {
|
||||||
var app = new Vue({
|
CheckinRulesVisualization: CheckinRulesVisualization.default,
|
||||||
el: '#rules-editor',
|
}
|
||||||
components: {
|
if (typeof CheckinRule !== "undefined") {
|
||||||
|
Vue.component('checkin-rule', CheckinRule.default);
|
||||||
|
components = {
|
||||||
CheckinRulesEditor: CheckinRulesEditor.default,
|
CheckinRulesEditor: CheckinRulesEditor.default,
|
||||||
CheckinRulesVisualization: CheckinRulesVisualization.default,
|
CheckinRulesVisualization: CheckinRulesVisualization.default,
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
var app = new Vue({
|
||||||
|
el: '#rules-editor',
|
||||||
|
components: components,
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
rules: {},
|
rules: {},
|
||||||
@@ -187,17 +193,23 @@ $(function () {
|
|||||||
},
|
},
|
||||||
created: function () {
|
created: function () {
|
||||||
this.rules = JSON.parse($("#id_rules").val());
|
this.rules = JSON.parse($("#id_rules").val());
|
||||||
this.items = JSON.parse($("#items").html());
|
if ($("#items").length) {
|
||||||
|
this.items = JSON.parse($("#items").html());
|
||||||
|
|
||||||
var root = this.$root
|
var root = this.$root
|
||||||
function _update() {
|
|
||||||
root.all_products = $("#id_all_products").prop("checked")
|
function _update() {
|
||||||
root.limit_products = $("input[name=limit_products]:checked").map(function () { return parseInt($(this).val()) }).toArray()
|
root.all_products = $("#id_all_products").prop("checked")
|
||||||
|
root.limit_products = $("input[name=limit_products]:checked").map(function () {
|
||||||
|
return parseInt($(this).val())
|
||||||
|
}).toArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#id_all_products, input[name=limit_products]").on("change", function () {
|
||||||
|
_update();
|
||||||
|
})
|
||||||
|
_update()
|
||||||
}
|
}
|
||||||
$("#id_all_products, input[name=limit_products]").on("change", function () {
|
|
||||||
_update();
|
|
||||||
})
|
|
||||||
_update()
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
rules: {
|
rules: {
|
||||||
|
|||||||
@@ -106,6 +106,9 @@ export default {
|
|||||||
let [new_children, new_tails] = _add_to_graph(operand)
|
let [new_children, new_tails] = _add_to_graph(operand)
|
||||||
for (let new_child of new_tails) {
|
for (let new_child of new_tails) {
|
||||||
graph.nodes_by_id[new_child].children.push(...children)
|
graph.nodes_by_id[new_child].children.push(...children)
|
||||||
|
for (let c of children) {
|
||||||
|
graph.nodes_by_id[c].parent = graph.nodes_by_id[new_child]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (tails === null) {
|
if (tails === null) {
|
||||||
tails = new_tails
|
tails = new_tails
|
||||||
|
|||||||
@@ -3,20 +3,27 @@
|
|||||||
<path v-for="e in edges" :d="e" class="edge"></path>
|
<path v-for="e in edges" :d="e" class="edge"></path>
|
||||||
<path v-if="rootEdge" :d="rootEdge" class="edge"></path>
|
<path v-if="rootEdge" :d="rootEdge" class="edge"></path>
|
||||||
<path v-if="!node.children.length" :d="checkEdge" class="edge"></path>
|
<path v-if="!node.children.length" :d="checkEdge" class="edge"></path>
|
||||||
<rect :width="boxWidth" :height="boxHeight" :x="x" :y="y" class="node" rx="5">
|
<rect :width="boxWidth" :height="boxHeight" :x="x" :y="y" :class="nodeClass" rx="5">
|
||||||
</rect>
|
</rect>
|
||||||
|
|
||||||
<foreignObject :width="boxWidth - 10" :height="boxHeight - 10" :x="x + 5" :y="y + 5">
|
<foreignObject :width="boxWidth - 10" :height="boxHeight - 10" :x="x + 5" :y="y + 5">
|
||||||
<div xmlns="http://www.w3.org/1999/xhtml" class="text">
|
<div xmlns="http://www.w3.org/1999/xhtml" class="text">
|
||||||
<span v-if="vardata && vardata.type === 'int'">
|
<span v-if="vardata && vardata.type === 'int'">
|
||||||
<span v-if="variable.startsWith('entries_')" class="fa fa-sign-in"></span>
|
<span v-if="variable.startsWith('entries_')" class="fa fa-sign-in"></span>
|
||||||
{{ vardata.label }}
|
{{ vardata.label }}
|
||||||
<br>
|
<br>
|
||||||
|
<span v-if="varresult !== null">
|
||||||
|
{{varresult}}
|
||||||
|
</span>
|
||||||
<strong>
|
<strong>
|
||||||
{{ op.label }} {{ rightoperand }}
|
{{ op.label }} {{ rightoperand }}
|
||||||
</strong>
|
</strong>
|
||||||
</span>
|
</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">
|
||||||
|
{{varresult}}
|
||||||
|
</span>
|
||||||
<strong>
|
<strong>
|
||||||
{{ op.label }}<br>
|
{{ op.label }}<br>
|
||||||
<span v-if="rightoperand.buildTime[0] === 'custom'">
|
<span v-if="rightoperand.buildTime[0] === 'custom'">
|
||||||
@@ -37,7 +44,11 @@
|
|||||||
</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 }}<br>
|
<span class="fa fa-ticket"></span> {{ vardata.label }}
|
||||||
|
<span v-if="varresult !== null">
|
||||||
|
({{varresult}})
|
||||||
|
</span>
|
||||||
|
<br>
|
||||||
<strong>
|
<strong>
|
||||||
{{ rightoperand.objectList.map((o) => o.lookup[2]).join(", ") }}
|
{{ rightoperand.objectList.map((o) => o.lookup[2]).join(", ") }}
|
||||||
</strong>
|
</strong>
|
||||||
@@ -45,9 +56,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</foreignObject>
|
</foreignObject>
|
||||||
|
|
||||||
<g v-if="!node.children.length" :transform="`translate(${x + boxWidth + 25}, ${y + boxHeight/2 - 15})`">
|
<g v-if="result === false" :transform="`translate(${x + boxWidth - 15}, ${y - 10})`">
|
||||||
<path d="m 25.078125,11.835938 c 0,-0.332032 -0.117188,-0.664063 -0.351563,-0.898438 L 22.949219,9.1796875 c -0.234375,-0.234375 -0.546875,-0.3710937 -0.878907,-0.3710937 -0.332031,0 -0.644531,0.1367187 -0.878906,0.3710937 L 13.222656,17.128906 8.8085938,12.714844 C 8.5742188,12.480469 8.2617188,12.34375 7.9296875,12.34375 c -0.3320313,0 -0.6445313,0.136719 -0.8789063,0.371094 l -1.7773437,1.757812 c -0.234375,0.234375 -0.3515625,0.566407 -0.3515625,0.898438 0,0.332031 0.1171875,0.644531 0.3515625,0.878906 l 7.0703125,7.070312 c 0.234375,0.234375 0.566406,0.371094 0.878906,0.371094 0.332032,0 0.664063,-0.136719 0.898438,-0.371094 L 24.726562,12.714844 c 0.234375,-0.234375 0.351563,-0.546875 0.351563,-0.878906 z M 30,15 C 30,23.28125 23.28125,30 15,30 6.71875,30 0,23.28125 0,15 0,6.71875 6.71875,0 15,0 23.28125,0 30,6.71875 30,15 Z"
|
<ellipse fill="#fff" cx="14.685823" cy="14.318233" rx="12.140151" ry="11.55523" />
|
||||||
class="check"/>
|
<path d="M 15,0 C 23.28125,0 30,6.71875 30,15 30,23.28125 23.28125,30 15,30 6.71875,30 0,23.28125 0,15 0,6.71875 6.71875,0 15,0 Z m 2.5,24.35547 V 20.64453 C 17.5,20.29297 17.22656,20 16.89453,20 h -3.75 C 12.79297,20 12.5,20.29297 12.5,20.64453 v 3.71094 C 12.5,24.70703 12.79297,25 13.14453,25 h 3.75 C 17.22656,25 17.5,24.70703 17.5,24.35547 Z M 17.4609,17.63672 17.81246,5.50781 c 0,-0.13672 -0.0586,-0.27343 -0.19531,-0.35156 C 17.49996,5.05855 17.32418,5 17.1484,5 h -4.29688 c -0.17578,0 -0.35156,0.0586 -0.46875,0.15625 -0.13672,0.0781 -0.19531,0.21484 -0.19531,0.35156 l 0.33203,12.12891 c 0,0.27344 0.29297,0.48828 0.66406,0.48828 h 3.61329 c 0.35156,0 0.64453,-0.21484 0.66406,-0.48828 z"
|
||||||
|
class="error" />
|
||||||
|
</g>
|
||||||
|
<g v-if="result === true" :transform="`translate(${x + boxWidth - 15}, ${y - 10})`">
|
||||||
|
<ellipse fill="#fff" cx="14.685823" cy="14.318233" rx="12.140151" ry="11.55523" />
|
||||||
|
<path d="m 25.078125,11.835938 c 0,-0.332032 -0.117188,-0.664063 -0.351563,-0.898438 L 22.949219,9.1796875 c -0.234375,-0.234375 -0.546875,-0.3710937 -0.878907,-0.3710937 -0.332031,0 -0.644531,0.1367187 -0.878906,0.3710937 L 13.222656,17.128906 8.8085938,12.714844 C 8.5742188,12.480469 8.2617188,12.34375 7.9296875,12.34375 c -0.3320313,0 -0.6445313,0.136719 -0.8789063,0.371094 l -1.7773437,1.757812 c -0.234375,0.234375 -0.3515625,0.566407 -0.3515625,0.898438 0,0.332031 0.1171875,0.644531 0.3515625,0.878906 l 7.0703125,7.070312 c 0.234375,0.234375 0.566406,0.371094 0.878906,0.371094 0.332032,0 0.664063,-0.136719 0.898438,-0.371094 L 24.726562,12.714844 c 0.234375,-0.234375 0.351563,-0.546875 0.351563,-0.878906 z M 30,15 C 30,23.28125 23.28125,30 15,30 6.71875,30 0,23.28125 0,15 0,6.71875 6.71875,0 15,0 23.28125,0 30,6.71875 30,15 Z"
|
||||||
|
class="check"/>
|
||||||
|
</g>
|
||||||
|
<g v-if="!node.children.length && (resultInclParents === null || resultInclParents === true)" :transform="`translate(${x + boxWidth + 25}, ${y + boxHeight/2 - 15})`">
|
||||||
|
<path d="m 25.078125,11.835938 c 0,-0.332032 -0.117188,-0.664063 -0.351563,-0.898438 L 22.949219,9.1796875 c -0.234375,-0.234375 -0.546875,-0.3710937 -0.878907,-0.3710937 -0.332031,0 -0.644531,0.1367187 -0.878906,0.3710937 L 13.222656,17.128906 8.8085938,12.714844 C 8.5742188,12.480469 8.2617188,12.34375 7.9296875,12.34375 c -0.3320313,0 -0.6445313,0.136719 -0.8789063,0.371094 l -1.7773437,1.757812 c -0.234375,0.234375 -0.3515625,0.566407 -0.3515625,0.898438 0,0.332031 0.1171875,0.644531 0.3515625,0.878906 l 7.0703125,7.070312 c 0.234375,0.234375 0.566406,0.371094 0.878906,0.371094 0.332032,0 0.664063,-0.136719 0.898438,-0.371094 L 24.726562,12.714844 c 0.234375,-0.234375 0.351563,-0.546875 0.351563,-0.878906 z M 30,15 C 30,23.28125 23.28125,30 15,30 6.71875,30 0,23.28125 0,15 0,6.71875 6.71875,0 15,0 23.28125,0 30,6.71875 30,15 Z"
|
||||||
|
class="check"/>
|
||||||
|
</g>
|
||||||
|
<g v-if="!node.children.length && (resultInclParents === false)" :transform="`translate(${x + boxWidth + 25}, ${y + boxHeight/2 - 15})`">
|
||||||
|
<path d="M 15,0 C 23.28125,0 30,6.71875 30,15 30,23.28125 23.28125,30 15,30 6.71875,30 0,23.28125 0,15 0,6.71875 6.71875,0 15,0 Z m 2.5,24.35547 V 20.64453 C 17.5,20.29297 17.22656,20 16.89453,20 h -3.75 C 12.79297,20 12.5,20.29297 12.5,20.64453 v 3.71094 C 12.5,24.70703 12.79297,25 13.14453,25 h 3.75 C 17.22656,25 17.5,24.70703 17.5,24.35547 Z M 17.4609,17.63672 17.81246,5.50781 c 0,-0.13672 -0.0586,-0.27343 -0.19531,-0.35156 C 17.49996,5.05855 17.32418,5 17.1484,5 h -4.29688 c -0.17578,0 -0.35156,0.0586 -0.46875,0.15625 -0.13672,0.0781 -0.19531,0.21484 -0.19531,0.35156 l 0.33203,12.12891 c 0,0.27344 0.29297,0.48828 0.66406,0.48828 h 3.61329 c 0.35156,0 0.64453,-0.21484 0.66406,-0.48828 z"
|
||||||
|
class="error" />
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
</template>
|
</template>
|
||||||
@@ -121,6 +146,14 @@
|
|||||||
vardata () {
|
vardata () {
|
||||||
return this.$root.VARS[this.variable];
|
return this.$root.VARS[this.variable];
|
||||||
},
|
},
|
||||||
|
varresult () {
|
||||||
|
const op = this.operator;
|
||||||
|
if (this.node.rule[op] && this.node.rule[op][0]) {
|
||||||
|
return this.node.rule[op][0]["__result"];
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
},
|
||||||
rightoperand () {
|
rightoperand () {
|
||||||
const op = this.operator;
|
const op = this.operator;
|
||||||
if (this.node.rule[op] && typeof this.node.rule[op][1] !== "undefined") {
|
if (this.node.rule[op] && typeof this.node.rule[op][1] !== "undefined") {
|
||||||
@@ -136,8 +169,30 @@
|
|||||||
return this.node.rule[this.operator]
|
return this.node.rule[this.operator]
|
||||||
},
|
},
|
||||||
operator: function () {
|
operator: function () {
|
||||||
return Object.keys(this.node.rule)[0];
|
return Object.keys(this.node.rule).filter(function (k) { return !k.startsWith("__") })[0];
|
||||||
},
|
},
|
||||||
|
result: function () {
|
||||||
|
return typeof this.node.rule.__result == "undefined" ? null : !!this.node.rule.__result
|
||||||
|
},
|
||||||
|
resultInclParents: function () {
|
||||||
|
if (typeof this.node.rule.__result == "undefined")
|
||||||
|
return null
|
||||||
|
|
||||||
|
function _p(node) {
|
||||||
|
if (node.parent) {
|
||||||
|
return node.rule.__result && _p(node.parent)
|
||||||
|
}
|
||||||
|
return node.rule.__result
|
||||||
|
}
|
||||||
|
return _p(this.node)
|
||||||
|
},
|
||||||
|
nodeClass: function () {
|
||||||
|
return {
|
||||||
|
"node": true,
|
||||||
|
"node-true": this.result === true,
|
||||||
|
"node-false": this.result === false,
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
df (val) {
|
df (val) {
|
||||||
|
|||||||
@@ -716,6 +716,12 @@ table td > .checkbox input[type="checkbox"] {
|
|||||||
stroke: $brand-primary;
|
stroke: $brand-primary;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.node-false {
|
||||||
|
stroke: $brand-danger;
|
||||||
|
}
|
||||||
|
.node-true {
|
||||||
|
stroke: $brand-success;
|
||||||
|
}
|
||||||
.edge {
|
.edge {
|
||||||
stroke: $gray-light;
|
stroke: $gray-light;
|
||||||
stroke-width: 2px;
|
stroke-width: 2px;
|
||||||
@@ -724,6 +730,9 @@ table td > .checkbox input[type="checkbox"] {
|
|||||||
.check {
|
.check {
|
||||||
fill: $brand-success;
|
fill: $brand-success;
|
||||||
}
|
}
|
||||||
|
.error {
|
||||||
|
fill: $brand-danger;
|
||||||
|
}
|
||||||
.text {
|
.text {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -739,6 +748,10 @@ table td > .checkbox input[type="checkbox"] {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
&:hover .tools {
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.maximized {
|
&.maximized {
|
||||||
|
|||||||
@@ -830,6 +830,30 @@ tbody th {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nomargin-top {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkin-sim-result {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
.fa {
|
||||||
|
color: white;
|
||||||
|
font-size: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.checkin-sim-result-status-ok {
|
||||||
|
background: $brand-success;
|
||||||
|
}
|
||||||
|
.checkin-sim-result-status-incomplete {
|
||||||
|
background: $brand-primary;
|
||||||
|
}
|
||||||
|
.checkin-sim-result-status-error {
|
||||||
|
background: $brand-danger;
|
||||||
|
}
|
||||||
|
.checkin-sim-result-status-error.checkin-sim-result-reason-already_redeemed {
|
||||||
|
background: $brand-warning;
|
||||||
|
}
|
||||||
|
|
||||||
.withoutjs {
|
.withoutjs {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
|
|||||||
Reference in New Issue
Block a user