mirror of
https://github.com/pretix/pretix.git
synced 2026-05-06 15:24:02 +00:00
Store all check-in attempts, not only successful ones (#2074)
This commit is contained in:
@@ -48,15 +48,16 @@ from django.utils.formats import date_format, localize
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import get_current_timezone, make_aware, now
|
||||
from django.utils.translation import gettext, gettext_lazy as _, pgettext_lazy
|
||||
from django_scopes.forms import SafeModelChoiceField
|
||||
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.forms.widgets import (
|
||||
DatePickerWidget, SplitDateTimePickerWidget,
|
||||
)
|
||||
from pretix.base.models import (
|
||||
Checkin, Event, EventMetaProperty, EventMetaValue, Invoice, InvoiceAddress,
|
||||
Item, Order, OrderPayment, OrderPosition, OrderRefund, Organizer, Question,
|
||||
QuestionAnswer, SubEvent,
|
||||
Checkin, CheckinList, Device, Event, EventMetaProperty, EventMetaValue,
|
||||
Gate, Invoice, InvoiceAddress, Item, Order, OrderPayment, OrderPosition,
|
||||
OrderRefund, Organizer, Question, QuestionAnswer, SubEvent,
|
||||
)
|
||||
from pretix.base.signals import register_payment_providers
|
||||
from pretix.control.forms.widgets import Select2
|
||||
@@ -1736,3 +1737,127 @@ class OverviewFilterForm(FilterForm):
|
||||
self.fields['subevent'].widget.choices = self.fields['subevent'].choices
|
||||
elif 'subevent':
|
||||
del self.fields['subevent']
|
||||
|
||||
|
||||
class CheckinFilterForm(FilterForm):
|
||||
status = forms.ChoiceField(
|
||||
label=_('Status'),
|
||||
choices=(
|
||||
('', _('All check-ins')),
|
||||
('successful', _('Successful check-ins')),
|
||||
('unsuccessful', _('Unsuccessful check-ins')),
|
||||
),
|
||||
required=False
|
||||
)
|
||||
type = forms.ChoiceField(
|
||||
label=_('Scan type'),
|
||||
choices=[
|
||||
('', _('All directions')),
|
||||
] + list(Checkin.CHECKIN_TYPES),
|
||||
required=False
|
||||
)
|
||||
itemvar = forms.ChoiceField(
|
||||
label=_("Product"),
|
||||
required=False
|
||||
)
|
||||
device = SafeModelChoiceField(
|
||||
label=_('Device'),
|
||||
empty_label=_('All devices'),
|
||||
queryset=Device.objects.none(),
|
||||
required=False
|
||||
)
|
||||
gate = SafeModelChoiceField(
|
||||
label=_('Gate'),
|
||||
empty_label=_('All gates'),
|
||||
queryset=Gate.objects.none(),
|
||||
required=False
|
||||
)
|
||||
checkin_list = SafeModelChoiceField(queryset=CheckinList.objects.none(), required=False) # overridden later
|
||||
datetime_from = forms.SplitDateTimeField(
|
||||
widget=SplitDateTimePickerWidget(attrs={
|
||||
}),
|
||||
label=pgettext_lazy('filter', 'Start date'),
|
||||
required=False,
|
||||
)
|
||||
datetime_until = forms.SplitDateTimeField(
|
||||
widget=SplitDateTimePickerWidget(attrs={
|
||||
}),
|
||||
label=pgettext_lazy('filter', 'End date'),
|
||||
required=False,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.event = kwargs.pop('event')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['device'].queryset = self.event.organizer.devices.all()
|
||||
self.fields['gate'].queryset = self.event.organizer.gates.all()
|
||||
|
||||
self.fields['checkin_list'].queryset = self.event.checkin_lists.all()
|
||||
self.fields['checkin_list'].widget = Select2(
|
||||
attrs={
|
||||
'data-model-select2': 'generic',
|
||||
'data-select2-url': reverse('control:event.orders.checkinlists.select2', kwargs={
|
||||
'event': self.event.slug,
|
||||
'organizer': self.event.organizer.slug,
|
||||
}),
|
||||
'data-placeholder': _('Check-in list'),
|
||||
}
|
||||
)
|
||||
self.fields['checkin_list'].widget.choices = self.fields['checkin_list'].choices
|
||||
self.fields['checkin_list'].label = _('Check-in list')
|
||||
|
||||
choices = [('', _('All products'))]
|
||||
for i in self.event.items.prefetch_related('variations').all():
|
||||
variations = list(i.variations.all())
|
||||
if variations:
|
||||
choices.append((str(i.pk), _('{product} – Any variation').format(product=i.name)))
|
||||
for v in variations:
|
||||
choices.append(('%d-%d' % (i.pk, v.pk), '%s – %s' % (i.name, v.value)))
|
||||
else:
|
||||
choices.append((str(i.pk), i.name))
|
||||
self.fields['itemvar'].choices = choices
|
||||
|
||||
def filter_qs(self, qs):
|
||||
fdata = self.cleaned_data
|
||||
|
||||
if fdata.get('status'):
|
||||
s = fdata.get('status')
|
||||
if s == 'successful':
|
||||
qs = qs.filter(successful=True)
|
||||
elif s == 'unsuccessful':
|
||||
qs = qs.filter(successful=False)
|
||||
|
||||
if fdata.get('type'):
|
||||
qs = qs.filter(type=fdata.get('type'))
|
||||
|
||||
if fdata.get('itemvar'):
|
||||
if '-' in fdata.get('itemvar'):
|
||||
qs = qs.alias(
|
||||
item_id=Coalesce('raw_item_id', 'position__item_id'),
|
||||
variation_id=Coalesce('raw_variation_id', 'position__variation_id'),
|
||||
).filter(
|
||||
item_id=fdata.get('itemvar').split('-')[0],
|
||||
variation_id=fdata.get('itemvar').split('-')[1]
|
||||
)
|
||||
else:
|
||||
qs = qs.alias(
|
||||
item_id=Coalesce('raw_item_id', 'position__item_id'),
|
||||
).filter(item_id=fdata.get('itemvar'))
|
||||
|
||||
if fdata.get('device'):
|
||||
qs = qs.filter(device_id=fdata.get('device').pk)
|
||||
|
||||
if fdata.get('gate'):
|
||||
qs = qs.filter(gate_id=fdata.get('gate').pk)
|
||||
|
||||
if fdata.get('checkin_list'):
|
||||
qs = qs.filter(list_id=fdata.get('checkin_list').pk)
|
||||
|
||||
if fdata.get('datetime_from'):
|
||||
qs = qs.filter(datetime__gte=fdata.get('datetime_from'))
|
||||
|
||||
if fdata.get('datetime_until'):
|
||||
qs = qs.filter(datetime__lte=fdata.get('datetime_until'))
|
||||
|
||||
return qs
|
||||
|
||||
@@ -297,7 +297,15 @@ def get_event_navigation(request: HttpRequest):
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': 'event.orders.checkin' in url.url_name,
|
||||
'active': 'event.orders.checkinlists' in url.url_name,
|
||||
},
|
||||
{
|
||||
'label': _('Check-in history'),
|
||||
'url': reverse('control:event.orders.checkins', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': 'event.orders.checkins' in url.url_name,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
170
src/pretix/control/templates/pretixcontrol/checkin/checkins.html
Normal file
170
src/pretix/control/templates/pretixcontrol/checkin/checkins.html
Normal file
@@ -0,0 +1,170 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Check-in history" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Check-in history" %}</h1>
|
||||
<form class="" action="" method="get">
|
||||
<div class="row filter-form">
|
||||
<div class="col-md-3 col-sm-6 col-xs-12">
|
||||
{% bootstrap_field filter_form.checkin_list 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.type layout='inline' %}
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6 col-xs-12">
|
||||
{% bootstrap_field filter_form.device layout='inline' %}
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6 col-xs-12">
|
||||
{% bootstrap_field filter_form.datetime_from layout='inline' %}
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6 col-xs-12">
|
||||
{% bootstrap_field filter_form.datetime_until layout='inline' %}
|
||||
</div>
|
||||
<div class="col-md-2 col-sm-6 col-xs-12">
|
||||
{% bootstrap_field filter_form.gate layout='inline' %}
|
||||
</div>
|
||||
<div class="col-md-2 col-sm-6 col-xs-12">
|
||||
{% bootstrap_field filter_form.itemvar layout='inline' %}
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-6">
|
||||
<button class="btn btn-block btn-primary" type="submit">
|
||||
<span class="fa fa-filter"></span>
|
||||
<span class="hidden-md">{% trans "Filter" %}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% if checkins|length == 0 %}
|
||||
<div class="empty-collection">
|
||||
<p>
|
||||
{% if request.GET %}
|
||||
{% trans "Your search did not match any check-ins." %}
|
||||
{% else %}
|
||||
{% blocktrans trimmed %}
|
||||
You haven't scanned any tickets yet.
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-quotas">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Time of scan" %}</th>
|
||||
<th>{% trans "Scan type" %}<br>{% trans "Check-in list" %}</th>
|
||||
<th>{% trans "Result" %}</th>
|
||||
<th>{% trans "Ticket" %}<br>{% trans "Product" %}</th>
|
||||
<th>{% trans "Device" %}<br>{% trans "Gate" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for c in checkins %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ c.datetime|date:"SHORT_DATETIME_FORMAT" }}
|
||||
{% if c.type == "exit" %}
|
||||
{% if c.auto_checked_in %}
|
||||
<span class="fa fa-fw fa-hourglass-end" data-toggle="tooltip_html"
|
||||
title="{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically marked not present: {{ date }}{% endblocktrans %}"></span>
|
||||
{% endif %}
|
||||
{% elif c.forced and c.successful %}
|
||||
<span class="fa fa-fw fa-warning" data-toggle="tooltip_html"
|
||||
title="{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Additional entry scan: {{ date }}{% endblocktrans %}"></span>
|
||||
{% elif c.forced and not c.successful %}
|
||||
<br>
|
||||
<small class="text-muted">{% trans "Failed in offline mode" %}</small>
|
||||
{% elif c.auto_checked_in %}
|
||||
<span class="fa fa-fw fa-magic" data-toggle="tooltip_html"
|
||||
title="{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically checked in: {{ date }}{% endblocktrans %}"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if c.type == "exit" %}<span class="fa fa-fw fa-sign-out"></span>{% endif %}
|
||||
{% if c.type == "entry" %}<span class="fa fa-fw fa-sign-in"></span>{% endif %}
|
||||
{{ c.get_type_display }}
|
||||
<br>
|
||||
<small>
|
||||
<a href="{% url "control:event.orders.checkinlists.show" organizer=request.event.organizer.slug event=request.event.slug list=c.list.id %}">{{ c.list }}</a>
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
{% if c.successful %}
|
||||
<span class="label label-success">
|
||||
<span class="fa fa-fw fa-check"></span> {% trans "Successful" context "checkin_result" %}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="label label-danger">
|
||||
<span class="fa fa-fw fa-exclamation-triangle"></span>
|
||||
{% trans "Denied" context "checkin_result" %}
|
||||
</span>
|
||||
<br>
|
||||
<small>
|
||||
{{ c.get_error_reason_display }}
|
||||
{% if c.error_explanation %}
|
||||
<br>
|
||||
{{ c.error_explanation }}
|
||||
{% endif %}
|
||||
</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if c.position %}
|
||||
<span class="fa fa-user fa-fw"></span>
|
||||
<strong>
|
||||
<a href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=c.position.order.code %}">{{ c.position.order.code }}</a>-{{ c.position.positionid }}
|
||||
</strong>
|
||||
{% if c.position.attendee_name %}
|
||||
<br>
|
||||
<small>
|
||||
{{ c.position.attendee_name }}
|
||||
</small>
|
||||
{% endif %}
|
||||
{% if c.position.item %}
|
||||
<br>
|
||||
<small>
|
||||
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=c.position.id %}">
|
||||
{{ c.position.item }}{% if c.position.variation %} –
|
||||
{{ c.position.variation }}{% endif %}
|
||||
</a>
|
||||
</small>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="fa fa-qrcode fa-fw"></span>
|
||||
<span title="{{ c.raw_barcode }}">
|
||||
{{ c.raw_barcode|slice:":16" }}{% if c.raw_barcode|length > 16 %}…{% endif %}
|
||||
</span>
|
||||
{% if c.raw_item %}
|
||||
<br>
|
||||
<small>
|
||||
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=c.raw_item.id %}">
|
||||
{{ c.raw_item }}{% if c.raw_variation %} – {{ c.raw_variation }}{% endif %}
|
||||
</a>
|
||||
</small>
|
||||
{% endif %}
|
||||
{% if c.raw_subevent %}
|
||||
<br>
|
||||
<small>
|
||||
{{ c.raw_subevent }}{% if c.raw_variation %} – {{ c.raw_variation }}{% endif %}
|
||||
</small>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ c.device|default:"" }}
|
||||
{% if c.gate %}
|
||||
<br><small>{{ c.gate }}</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
{% endblock %}
|
||||
@@ -324,19 +324,21 @@
|
||||
– {{ line.variation }}
|
||||
{% endif %}
|
||||
{% if line.checkins.all %}
|
||||
{% for c in line.checkins.all %}
|
||||
{% if c.type == "exit" %}
|
||||
{% for c in line.all_checkins.all %}
|
||||
{% if not c.successful %}
|
||||
<span class="fa fa-fw fa-exclamation-circle text-danger" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Denied scan: {{ date }}{% endblocktrans %}<br>{{ c.get_error_reason_display }}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
|
||||
{% elif c.type == "exit" %}
|
||||
{% if c.auto_checked_in %}
|
||||
<span class="fa fa-fw fa-hourglass-end" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically marked not present: {{ date }}{% endblocktrans %}"></span>
|
||||
<span class="fa fa-fw text-success fa-hourglass-end" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically marked not present: {{ date }}{% endblocktrans %}"></span>
|
||||
{% else %}
|
||||
<span class="fa fa-fw fa-sign-out" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Exit scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
|
||||
<span class="fa fa-fw text-success fa-sign-out" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Exit scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
|
||||
{% endif %}
|
||||
{% elif c.forced %}
|
||||
<span class="fa fa-fw fa-warning" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Additional entry scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
|
||||
<span class="fa fa-fw fa-warning text-warning" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Additional entry scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
|
||||
{% elif c.auto_checked_in %}
|
||||
<span class="fa fa-fw fa-magic" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically checked in: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
|
||||
<span class="fa fa-fw fa-magic text-success" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically checked in: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
|
||||
{% else %}
|
||||
<span class="fa fa-fw fa-check" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Entry scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
|
||||
<span class="fa fa-fw fa-check text-success" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Entry scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
@@ -364,6 +364,7 @@ urlpatterns = [
|
||||
re_path(r'^waitinglist/auto_assign$', waitinglist.AutoAssign.as_view(), name='event.orders.waitinglist.auto'),
|
||||
re_path(r'^waitinglist/(?P<entry>\d+)/delete$', waitinglist.EntryDelete.as_view(),
|
||||
name='event.orders.waitinglist.delete'),
|
||||
re_path(r'^checkins/$', checkin.CheckinListView.as_view(), name='event.orders.checkins'),
|
||||
re_path(r'^checkinlists/$', checkin.CheckinListList.as_view(), name='event.orders.checkinlists'),
|
||||
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'),
|
||||
|
||||
@@ -50,7 +50,7 @@ from pretix.base.models import Checkin, Order, OrderPosition
|
||||
from pretix.base.models.checkin import CheckinList
|
||||
from pretix.base.signals import checkin_created
|
||||
from pretix.control.forms.checkin import CheckinListForm
|
||||
from pretix.control.forms.filter import CheckInFilterForm
|
||||
from pretix.control.forms.filter import CheckInFilterForm, CheckinFilterForm
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.control.views import CreateView, PaginationMixin, UpdateView
|
||||
from pretix.helpers.models import modelcopy
|
||||
@@ -371,3 +371,31 @@ class CheckinListDelete(EventPermissionRequiredMixin, DeleteView):
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
})
|
||||
|
||||
|
||||
class CheckinListView(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
model = Checkin
|
||||
context_object_name = 'checkins'
|
||||
permission = 'can_view_orders'
|
||||
template_name = 'pretixcontrol/checkin/checkins.html'
|
||||
|
||||
def get_queryset(self):
|
||||
qs = Checkin.all.filter(
|
||||
list__event=self.request.event,
|
||||
).select_related(
|
||||
'position', 'position', 'position__item', 'position__variation', 'position__subevent'
|
||||
).prefetch_related(
|
||||
'list', 'gate'
|
||||
)
|
||||
if self.filter_form.is_valid():
|
||||
qs = self.filter_form.filter_qs(qs)
|
||||
return qs
|
||||
|
||||
@cached_property
|
||||
def filter_form(self):
|
||||
return CheckinFilterForm(data=self.request.GET, event=self.request.event)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data()
|
||||
ctx['filter_form'] = self.filter_form
|
||||
return ctx
|
||||
|
||||
@@ -340,7 +340,7 @@ class OrderDetail(OrderView):
|
||||
).prefetch_related(
|
||||
'item__questions', 'issued_gift_cards',
|
||||
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options').select_related('question')),
|
||||
Prefetch('checkins', queryset=Checkin.objects.select_related('list').order_by('datetime')),
|
||||
Prefetch('all_checkins', queryset=Checkin.all.select_related('list').order_by('datetime')),
|
||||
).order_by('positionid')
|
||||
|
||||
positions = []
|
||||
|
||||
Reference in New Issue
Block a user