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

View File

@@ -31,7 +31,8 @@ from django_scopes.forms import (
)
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.widgets import Select2
@@ -177,3 +178,26 @@ class SimpleCheckinListForm(forms.ModelForm):
'subevent': SafeModelChoiceField,
'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,
)

View File

@@ -16,6 +16,11 @@
{% trans "Edit list configuration" %}
</a>
{% 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 }}"
class="btn btn-default" target="_blank">
<span class="fa fa-download"></span>

View File

@@ -12,7 +12,15 @@
{% endblock %}
{% block inside %}
{% 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 %}
<h1>{% trans "Check-in list" %}</h1>
{% endif %}

View File

@@ -133,6 +133,9 @@
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
<span class="fa fa-copy"></span>
</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.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 %}

View File

@@ -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 %}

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/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+)/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+)/change$', checkin.CheckinListUpdate.as_view(),
name='event.orders.checkinlists.edit'),

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
# <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
import dateutil.parser
@@ -32,14 +45,21 @@ from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.timezone import is_aware, make_aware, now
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.models import Checkin, Order, OrderPosition
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.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 (
CheckinFilterForm, CheckinListAttendeeFilterForm,
)
@@ -48,18 +68,6 @@ from pretix.control.views import CreateView, PaginationMixin, UpdateView
from pretix.helpers.compat import CompatDeleteView
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:
@@ -469,3 +477,63 @@ class CheckinListView(EventPermissionRequiredMixin, PaginationMixin, ListView):
ctx = super().get_context_data()
ctx['filter_form'] = self.filter_form
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)