Add check-in simulator (#3380)

This commit is contained in:
Raphael Michel
2023-06-13 14:57:24 +02:00
committed by GitHub
parent 4917249bab
commit 002416e435
15 changed files with 544 additions and 137 deletions
+70 -62
View File
@@ -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,
+80 -37
View File
@@ -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.'),
+25 -1
View File
@@ -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 %}
+1
View File
@@ -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'),
+82 -14
View File
@@ -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)
+1 -1
View File
@@ -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;