Fix #515 -- Add check-in lists (#693)

* Data model and migration

* Some backwards compatibility

* CRUD for checkin lists

* Show and perform checkins

* Correct numbers in table and dashboard widget

* event creation and cloning

* Allow to link specific exports and pass options per query

* Play with the CSV export

* PDF export

* Collapse exports by default

* Improve PDF exporter

* Addon stuff

* Subevent stuff, pretixdroid tests

* pretixdroid tests

* Add CRUD API

* Test compatibility

* Fix test

* DB-independent sorting behavior

* Add CRUD and coyp tests

* Re-enable pretixdroid plugin

* pretixdroid config

* Tests & fixes
This commit is contained in:
Raphael Michel
2017-12-04 18:12:23 +01:00
committed by GitHub
parent f1be7ed69d
commit 353dce789d
58 changed files with 2402 additions and 608 deletions

View File

@@ -32,6 +32,7 @@
<script type="text/javascript" src="{% static "pretixcontrol/js/sb-admin-2.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/main.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/quota.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/subevent.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/question.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/mail.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/typeahead.js" %}"></script>

View File

@@ -2,65 +2,70 @@
{% load i18n %}
{% load eventurl %}
{% load urlreplace %}
{% block title %}{% trans "Check-ins" %}{% endblock %}
{% load bootstrap3 %}
{% block title %}
{% blocktrans with name=checkinlist.name %}Check-in list: {{ name }}{% endblocktrans %}
{% endblock %}
{% block content %}
<h1>{% trans "Check-ins" %}</h1>
<p>
<form class="form-inline helper-display-inline" action="" method="get">
<select name="status" class="form-control">
<option value="">{% trans "All status" %}</option>
<option value="1" {% if request.GET.status == "1" %}selected="selected"{% endif %}>{% trans "Checked in" %}</option>
<option value="0" {% if request.GET.status == "0" %}selected="selected"{% endif %}>{% trans "Not checked in" %}</option>
</select>
<select name="item" class="form-control">
<option value="">{% trans "All products" %}</option>
{% for item in items %}
<option value="{{ item.id }}"
{% if request.GET.item|add:0 == item.id %}selected="selected"{% endif %}>
{{ item.name }}
</option>
{% endfor %}
</select>
{% if request.event.has_subevents %}
<select name="subevent" class="form-control">
<option value="">{% trans "All dates" context "subevent" %}</option>
{% for se in request.event.subevents.all %}
<option value="{{ se.id }}"
{% if request.GET.subevent|add:0 == se.id %}selected="selected"{% endif %}>
{{ se.name }} {{ se.get_date_range_display }}
</option>
{% endfor %}
</select>
{% endif %}
<input type="text" name="user" class="form-control" placeholder="{% trans "Search user" %}" value="{{ request.GET.user }}">
<button class="btn btn-primary" type="submit">{% trans "Filter" %}</button>
</form>
</p>
<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-edit"></span>
{% trans "Edit list" %}
</a>
{% endif %}
<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>
{% trans "PDF" %}
</a>
<a href="{% url "control:event.orders.export" event=request.event.slug organizer=request.event.organizer.slug %}?identifier=checkinlistcsv&checkinlistcsv-list={{ checkinlist.pk }}"
class="btn btn-default" target="_blank">
<span class="fa fa-download"></span>
{% trans "CSV" %}
</a>
</h1>
<form class="row filter-form" action="" method="get">
<div class="col-md-4 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.user layout='inline' %}
</div>
<div class="col-md-3 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.status layout='inline' %}
</div>
<div class="col-md-3 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.item layout='inline' %}
</div>
<div class="col-md-2 col-sm-6 col-xs-12">
<button class="btn btn-primary btn-block" type="submit">
<span class="fa fa-filter"></span>
<span class="hidden-md">
{% trans "Filter" %}
</span>
</button>
</div>
</form>
{% if entries|length == 0 %}
<div class="empty-collection">
<p>
{% blocktrans trimmed %}
No check-in record was found.
No attendee record was found.
{% endblocktrans %}
</p>
</div>
{% else %}
{% include "pretixcontrol/pagination.html" %}
<form method="post" action="">
{% csrf_token %}
<div class="table-responsive">
<table class="table table-condensed table-hover">
<thead>
<tr>
<th />
<th></th>
<th>{% trans "Order code" %} <a href="?{% url_replace request 'ordering' '-code'%}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'code'%}"><i class="fa fa-caret-up"></i></a></th>
<th>{% trans "Item" %} <a href="?{% url_replace request 'ordering' '-item'%}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'item'%}"><i class="fa fa-caret-up"></i></a></th>
{% if request.event.has_subevents %}
<th>{% trans "Date" context "subevent" %} <a href="?{% url_replace request 'ordering' '-subevent'%}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'subevent'%}"><i class="fa fa-caret-up"></i></a></th>
{% endif %}
<th>{% trans "Email" %} <a href="?{% url_replace request 'ordering' '-email'%}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'email'%}"><i class="fa fa-caret-up"></i></a></th>
<th>{% trans "Name" %} <a href="?{% url_replace request 'ordering' '-name'%}"><i class="fa fa-caret-down"></i></a>
@@ -73,20 +78,16 @@
</thead>
<tbody>
{% for e in entries %}
{% with e.checkins.first as checkin %}
<tr>
<td>
<input type="checkbox" name="checkin"
id="id_checkin" class=""
value="{{e.pk}}"/>
</td>
<td>
{% if "can_change_orders" in request.eventpermset %}
<input type="checkbox" name="checkin" id="id_checkin" class="" value="{{ e.pk }}"/>
{% endif %}
</td>
<td>
<strong><a href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=e.order.code %}">{{ e.order.code }}</a></strong>
</td>
<td>{{ e.item.name }}{% if e.variation %} {{ e.variation }}{% endif %}</td>
{% if request.event.has_subevents %}
<td>{{ e.subevent.name }} {{ e.subevent.get_date_range_display }}</td>
{% endif %}
<td>{{ e.order.email }}</td>
<td>
{% if e.addon_to %}
@@ -96,26 +97,27 @@
{% endif %}
</td>
<td>
{% if not checkin %}
{% if not e.last_checked_in %}
<span class="label label-danger">{% trans "Not checked in" %}</span>
{% else %}
<span class="label label-success">{% trans "Checked in" %}</span>
{% endif %}
</td>
<td>
{% if checkin %}
{{ checkin.datetime|date:"SHORT_DATETIME_FORMAT" }}
{% if e.last_checked_in %}
{{ e.last_checked_in_aware|date:"SHORT_DATETIME_FORMAT" }}
{% endif %}
</td>
</tr>
{% endwith %}
{% endfor %}
</tbody>
</table>
</div>
<button type="submit" class="btn btn-primary btn-save">
{% trans "Check-In selected attendees" %}
</button>
{% if "can_change_orders" in request.eventpermset %}
<button type="submit" class="btn btn-primary btn-save">
{% trans "Check-In selected attendees" %}
</button>
{% endif %}
</form>
{% include "pretixcontrol/pagination.html" %}
{% endif %}

View File

@@ -0,0 +1,25 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Delete check-in list" %}{% endblock %}
{% block inside %}
<h1>{% trans "Delete check-in list" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<p>{% blocktrans with name=checkinlist.name %}Are you sure you want to delete the check-in list <strong>{{ name }}</strong>?{% endblocktrans %}</p>
{% if checkinlist.checkins.exists > 0 %}
<p>{% blocktrans with num=checkinlist.checkins.count %}
This will delete the information of <strong>{{ num }}</strong> check-ins as well.
{% endblocktrans %}</p>
{% endif %}
<div class="form-group submit-group">
<a href="{% url "control:event.orders.checkinlists" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-danger btn-save">
{% trans "Delete" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,41 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}
{% if checkinlist %}
{% blocktrans with name=checkinlist.name %}Check-in list: {{ name }}{% endblocktrans %}
{% else %}
{% trans "Check-in list" %}
{% endif %}
{% endblock %}
{% block inside %}
{% if checkinlist %}
<h1>{% blocktrans with name=checkinlist.name %}Check-in list: {{ name }}{% endblocktrans %}</h1>
{% else %}
<h1>{% trans "Check-in list" %}</h1>
{% endif %}
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% bootstrap_form_errors form %}
<fieldset>
<legend>{% trans "General information" %}</legend>
{% bootstrap_field form.name layout="control" %}
{% if form.subevent %}
{% bootstrap_field form.subevent layout="control" %}
{% endif %}
<legend>{% trans "Products" %}</legend>
<p>
{% blocktrans trimmed %}
Please select the products or product variations that should be part of this check-in list.
{% endblocktrans %}
</p>
{% bootstrap_field form.all_products layout="control" %}
{% bootstrap_field form.limit_products layout="control" %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,125 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% block title %}{% trans "Check-in lists" %}{% endblock %}
{% block inside %}
<h1>{% trans "Check-in lists" %}</h1>
<p>
{% blocktrans trimmed %}
You can create check-in lists that you can use e.g. at the entrance of your event to track who is coming
and if they actually bought a ticket. You can do this process by printing out the list on paper, using this
web interface or by using one of our mobile or desktop apps to automatically scan tickets.
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
You can create multiple check-in lists to separate multiple parts of your event, for example if you have
separate entries for multiple ticket types. Different check-in lists are completely independent: If a ticket
shows up on two lists, it is valid once on every list. This might be useful if you run a festival with
festival passes that allow access to every or multiple performances as well as tickets only valid for single
performances.
{% endblocktrans %}
</p>
{% if request.event.has_subevents %}
<form class="form-inline helper-display-inline" action="" method="get">
<p>
{% if request.event.has_subevents %}
<select name="subevent" class="form-control">
<option value="">{% trans "All dates" context "subevent" %}</option>
{% for se in request.event.subevents.all %}
<option value="{{ se.id }}"
{% if request.GET.subevent|add:0 == se.id %}selected="selected"{% endif %}>
{{ se.name }} {{ se.get_date_range_display }}
</option>
{% endfor %}
</select>
{% endif %}
<button class="btn btn-primary" type="submit">{% trans "Filter" %}</button>
</p>
</form>
{% endif %}
{% if checkinlists|length == 0 %}
<div class="empty-collection">
<p>
{% if request.GET.subevent %}
{% trans "Your search did not match any check-in lists." %}
{% else %}
{% blocktrans trimmed %}
You haven't created any check-in lists yet.
{% endblocktrans %}
{% endif %}
</p>
{% if "can_change_event_settings" in request.eventpermset %}
<a href="{% url "control:event.orders.checkinlists.add" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i>
{% trans "Create a new check-in list" %}</a>
{% endif %}
</div>
{% else %}
{% if "can_change_event_settings" in request.eventpermset %}
<p>
<a href="{% url "control:event.orders.checkinlists.add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new check-in list" %}
</a>
</p>
{% endif %}
<div class="table-responsive">
<table class="table table-hover table-quotas">
<thead>
<tr>
<th>{% trans "Check-in lists" %}</th>
<th>{% trans "Checked in" %}</th>
{% if request.event.has_subevents %}
<th>{% trans "Date" context "subevent" %}</th>
{% endif %}
<th>{% trans "Products" %}</th>
<th class="action-col-2"></th>
</tr>
</thead>
<tbody>
{% for cl in checkinlists %}
<tr>
<td>
<strong><a href="{% url "control:event.orders.checkinlists.show" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}">{{ cl.name }}</a></strong>
</td>
<td>
<div class="quotabox availability">
<div class="progress">
<div class="progress-bar progress-bar-success progress-bar-{{ cl.percent }}">
</div>
</div>
<div class="numbers">
{{ cl.checkin_count|default_if_none:"0" }} / {{ cl.position_count|default_if_none:"0" }}
</div>
</div>
</td>
{% if request.event.has_subevents %}
<td>{{ cl.subevent.name }} {{ cl.subevent.get_date_range_display }}</td>
{% endif %}
<td>
{% if cl.all_products %}
<em>{% trans "All" %}</em>
{% else %}
<ul>
{% for item in cl.limit_products.all %}
<li>
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.id %}">{{ item.name }}</a>
</li>
{% endfor %}
</ul>
{% endif %}
</td>
<td class="text-right">
<a href="{% url "control:event.orders.checkinlists.show" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}" class="btn btn-default btn-sm"><i class="fa fa-eye"></i></a>
{% if "can_change_event_settings" in request.eventpermset %}
<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-edit"></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 %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% include "pretixcontrol/pagination.html" %}
{% endblock %}

View File

@@ -101,12 +101,6 @@
{% trans "Waiting list" %}
</a>
</li>
<li>
<a href="{% url 'control:event.orders.checkins' organizer=request.event.organizer.slug event=request.event.slug %}"
{% if url_name == "event.orders.checkins" %}class="active"{% endif %}>
{% trans "Check-ins" %}
</a>
</li>
</ul>
</li>
{% endif %}
@@ -119,6 +113,15 @@
</a>
</li>
{% endif %}
{% if 'can_view_orders' in request.eventpermset %}
<li>
<a href="{% url 'control:event.orders.checkinlists' organizer=request.event.organizer.slug event=request.event.slug %}"
{% if "event.orders.checkin" in url_name %}class="active"{% endif %}>
<i class="fa fa-check-square-o fa-fw"></i>
{% trans "Check-in lists" %}
</a>
</li>
{% endif %}
{% for nav in nav_event %}
<li>
<a href="{{ nav.url }}" {% if nav.active %}class="active"{% endif %}

View File

@@ -187,7 +187,9 @@
{{ line.variation }}
{% endif %}
{% if line.checkins.all %}
<span class="fa fa-check" data-toggle="tooltip" title="{% blocktrans trimmed with date=line.checkins.all.0.datetime|date:'d.m.Y H:i' %}First scanned: {{ date }}{% endblocktrans %}"></span>
{% for c in line.checkins.all %}
<span class="fa fa-fw fa-check" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'d.m.Y H:i' %}First scanned: {{ date }}{% endblocktrans %}"></span>
{% endfor %}
{% endif %}
{% if line.voucher %}
<br/><span class="fa fa-tags"></span> {% trans "Voucher code used:" %}

View File

@@ -4,23 +4,36 @@
{% load order_overview %}
{% block title %}{% trans "Data export" %}{% endblock %}
{% block content %}
<h1>{% trans "Data export" %}</h1>
<h1>
{% trans "Data export" %}
{% if "identifier" in request.GET %}
<a href="?" class="btn btn-default">{% trans "Show all" %}</a>
{% endif %}
</h1>
{% for e in exporters %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{{ e.verbose_name }}</h3>
<h3 class="panel-title">
<a class="collapsed" data-toggle="collapse" href="#{{ e.identifier }}">
{{ e.verbose_name }}
<i class="fa fa-angle-down collapse-indicator"></i>
</a>
</h3>
</div>
<div class="panel-body">
<form action="{% url "control:event.orders.export.do" event=request.event.slug organizer=request.organizer.slug %}"
method="post" class="form-horizontal" data-asynctask data-asynctask-download
data-asynctask-long>
{% csrf_token %}
<input type="hidden" name="exporter" value="{{ e.identifier }}" />
{% bootstrap_form e.form layout='horizontal' %}
<button class="btn btn-primary pull-right" type="submit">
<span class="icon icon-upload"></span> {% trans "Start export" %}
</button>
</form>
<div id="{{ e.identifier }}" class="panel-collapse collapse {% if "identifier" in request.GET %}in
{% endif %}">
<div class="panel-body">
<form action="{% url "control:event.orders.export.do" event=request.event.slug organizer=request.organizer.slug %}"
method="post" class="form-horizontal" data-asynctask data-asynctask-download
data-asynctask-long>
{% csrf_token %}
<input type="hidden" name="exporter" value="{{ e.identifier }}" />
{% bootstrap_form e.form layout='horizontal' %}
<button class="btn btn-primary pull-right" type="submit">
<span class="icon icon-upload"></span> {% trans "Start export" %}
</button>
</form>
</div>
</div>
</div>
{% endfor %}

View File

@@ -123,6 +123,72 @@
{% bootstrap_field f.price layout="control" %}
{% endfor %}
</fieldset>
<fieldset>
<legend>{% trans "Check-in lists" %}</legend>
<div class="formset" data-formset data-formset-prefix="{{ cl_formset.prefix }}">
{{ cl_formset.management_form }}
{% bootstrap_formset_errors cl_formset %}
<div data-formset-body>
{% for form in cl_formset %}
<div class="panel panel-default" data-formset-form>
<div class="sr-only">
{{ form.id }}
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="panel-heading">
<h4 class="panel-title">
<div class="row">
<div class="col-md-10">
{% bootstrap_field form.name layout='inline' form_group_class="" %}
</div>
<div class="col-md-2 text-right">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
</h4>
</div>
<div class="panel-body form-horizontal">
{% bootstrap_form_errors form %}
{% bootstrap_field form.all_products layout="control" %}
{% bootstrap_field form.limit_products layout="control" %}
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="panel panel-default" data-formset-form>
<div class="sr-only">
{{ cl_formset.empty_form.id }}
{% bootstrap_field cl_formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="panel-heading">
<h4 class="panel-title">
<div class="row">
<div class="col-md-10">
{% bootstrap_field cl_formset.empty_form.name layout='inline' form_group_class="" %}
</div>
<div class="col-md-2 text-right">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
</h4>
</div>
<div class="panel-body form-horizontal">
{% bootstrap_field cl_formset.empty_form.all_products layout="control" %}
{% bootstrap_field cl_formset.empty_form.limit_products layout="control" %}
</div>
</div>
{% endescapescript %}
</script>
<p>
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add a new check-in list" %}
</button>
</p>
</fieldset>
</div>
{% if subevent.pk %}
<div class="col-xs-12 col-lg-2">