Store all check-in attempts, not only successful ones (#2074)

This commit is contained in:
Raphael Michel
2021-06-05 13:00:58 +02:00
committed by GitHub
parent 9c3fc69176
commit c7ef79be90
29 changed files with 849 additions and 66 deletions

View File

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

View File

@@ -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,
},
]
})

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

View File

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

View File

@@ -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'),

View File

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

View File

@@ -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 = []