mirror of
https://github.com/pretix/pretix.git
synced 2026-05-06 15:24:02 +00:00
* 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:
31
src/pretix/control/forms/checkin.py
Normal file
31
src/pretix/control/forms/checkin.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from django import forms
|
||||
|
||||
from pretix.base.models.checkin import CheckinList
|
||||
|
||||
|
||||
class CheckinListForm(forms.ModelForm):
|
||||
def __init__(self, **kwargs):
|
||||
self.event = kwargs.pop('event')
|
||||
kwargs.pop('locales', None)
|
||||
super().__init__(**kwargs)
|
||||
self.fields['limit_products'].queryset = self.event.items.all()
|
||||
if self.event.has_subevents:
|
||||
self.fields['subevent'].queryset = self.event.subevents.all()
|
||||
self.fields['subevent'].required = True
|
||||
else:
|
||||
del self.fields['subevent']
|
||||
|
||||
class Meta:
|
||||
model = CheckinList
|
||||
localized_fields = '__all__'
|
||||
fields = [
|
||||
'name',
|
||||
'all_products',
|
||||
'limit_products',
|
||||
'subevent'
|
||||
]
|
||||
widgets = {
|
||||
'limit_products': forms.CheckboxSelectMultiple(attrs={
|
||||
'data-inverse-dependency': '<[name$=all_products]'
|
||||
}),
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
from django import forms
|
||||
from django.apps import apps
|
||||
from django.db.models import Exists, OuterRef, Q
|
||||
from django.db.models.functions import Concat
|
||||
from django.db.models import Exists, F, OuterRef, Q
|
||||
from django.db.models.functions import Coalesce, Concat
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
|
||||
from pretix.base.models import Event, Invoice, Item, Order, Organizer, SubEvent
|
||||
from pretix.base.signals import register_payment_providers
|
||||
from pretix.control.utils.i18n import i18ncomp
|
||||
from pretix.helpers.database import rolledback_transaction
|
||||
from pretix.helpers.database import FixedOrderBy, rolledback_transaction
|
||||
|
||||
PAYMENT_PROVIDERS = []
|
||||
|
||||
@@ -57,7 +57,10 @@ class FilterForm(forms.Form):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['ordering'] = forms.ChoiceField(
|
||||
choices=sum([[(a, b), ('-' + a, '-' + b)] for a, b in self.orders.items()], []),
|
||||
choices=sum([
|
||||
[(a, a), ('-' + a, '-' + a)]
|
||||
for a in self.orders.keys()
|
||||
], []),
|
||||
required=False
|
||||
)
|
||||
|
||||
@@ -136,7 +139,7 @@ class OrderFilterForm(FilterForm):
|
||||
qs = qs.filter(status=s)
|
||||
|
||||
if fdata.get('ordering'):
|
||||
qs = qs.order_by(dict(self.fields['ordering'].choices)[fdata.get('ordering')])
|
||||
qs = qs.order_by(self.orders[fdata.get('ordering')])
|
||||
|
||||
if fdata.get('provider'):
|
||||
qs = qs.filter(payment_provider=fdata.get('provider'))
|
||||
@@ -273,7 +276,7 @@ class SubEventFilterForm(FilterForm):
|
||||
)
|
||||
|
||||
if fdata.get('ordering'):
|
||||
qs = qs.order_by(dict(self.fields['ordering'].choices)[fdata.get('ordering')])
|
||||
qs = qs.order_by(self.orders[fdata.get('ordering')])
|
||||
|
||||
return qs
|
||||
|
||||
@@ -387,6 +390,94 @@ class EventFilterForm(FilterForm):
|
||||
)
|
||||
|
||||
if fdata.get('ordering'):
|
||||
qs = qs.order_by(dict(self.fields['ordering'].choices)[fdata.get('ordering')])
|
||||
qs = qs.order_by(self.orders[fdata.get('ordering')])
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
class CheckInFilterForm(FilterForm):
|
||||
orders = {
|
||||
'code': ('order__code', 'item__name'),
|
||||
'-code': ('-order__code', '-item__name'),
|
||||
'email': ('order__email', 'item__name'),
|
||||
'-email': ('-order__email', '-item__name'),
|
||||
'status': (FixedOrderBy(F('last_checked_in'), nulls_first=True, descending=True), 'order__code'),
|
||||
'-status': (FixedOrderBy(F('last_checked_in'), nulls_last=True), '-order__code'),
|
||||
'timestamp': (FixedOrderBy(F('last_checked_in'), nulls_first=True), 'order__code'),
|
||||
'-timestamp': (FixedOrderBy(F('last_checked_in'), nulls_last=True, descending=True), '-order__code'),
|
||||
'item': ('item__name', 'variation__value', 'order__code'),
|
||||
'-item': ('-item__name', '-variation__value', '-order__code'),
|
||||
'name': {'_order': F('display_name').asc(nulls_first=True),
|
||||
'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')},
|
||||
'-name': {'_order': F('display_name').desc(nulls_last=True),
|
||||
'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')},
|
||||
}
|
||||
|
||||
user = forms.CharField(
|
||||
label=_('Search attendee…'),
|
||||
widget=forms.TextInput(attrs={
|
||||
'placeholder': _('Search attendee…'),
|
||||
'autofocus': 'autofocus'
|
||||
}),
|
||||
required=False
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
label=_('Check-in status'),
|
||||
choices=(
|
||||
('', _('All attendees')),
|
||||
('1', _('Checked in')),
|
||||
('0', _('Not checked in')),
|
||||
),
|
||||
required=False,
|
||||
)
|
||||
item = forms.ModelChoiceField(
|
||||
label=_('Products'),
|
||||
queryset=Item.objects.none(),
|
||||
required=False,
|
||||
empty_label=_('All products')
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.event = kwargs.pop('event')
|
||||
self.list = kwargs.pop('list')
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.list.all_products:
|
||||
self.fields['item'].queryset = self.event.items.all()
|
||||
else:
|
||||
self.fields['item'].queryset = self.list.limit_products.all()
|
||||
|
||||
def filter_qs(self, qs):
|
||||
fdata = self.cleaned_data
|
||||
|
||||
if fdata.get('user'):
|
||||
u = fdata.get('user')
|
||||
qs = qs.filter(
|
||||
Q(order__email__icontains=u)
|
||||
| Q(attendee_name__icontains=u)
|
||||
| Q(attendee_email__icontains=u)
|
||||
| Q(order__invoice_address__name__icontains=u)
|
||||
| Q(order__invoice_address__company__icontains=u)
|
||||
)
|
||||
|
||||
if fdata.get('status'):
|
||||
s = fdata.get('status')
|
||||
if s == '1':
|
||||
qs = qs.filter(last_checked_in__isnull=False)
|
||||
elif s == '0':
|
||||
qs = qs.filter(last_checked_in__isnull=True)
|
||||
|
||||
if fdata.get('ordering'):
|
||||
ob = self.orders[fdata.get('ordering')]
|
||||
if isinstance(ob, dict):
|
||||
ob = dict(ob)
|
||||
o = ob.pop('_order')
|
||||
qs = qs.annotate(**ob).order_by(o)
|
||||
elif isinstance(ob, (list, tuple)):
|
||||
qs = qs.order_by(*ob)
|
||||
else:
|
||||
qs = qs.order_by(ob)
|
||||
|
||||
if fdata.get('item'):
|
||||
qs = qs.filter(item=fdata.get('item'))
|
||||
|
||||
return qs
|
||||
|
||||
@@ -123,3 +123,30 @@ class SubEventMetaValueForm(forms.ModelForm):
|
||||
widgets = {
|
||||
'value': forms.TextInput
|
||||
}
|
||||
|
||||
|
||||
class CheckinListFormSet(I18nInlineFormSet):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.event = kwargs.pop('event', None)
|
||||
self.locales = self.event.settings.get('locales')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@cached_property
|
||||
def items(self):
|
||||
return self.event.items.prefetch_related('variations').all()
|
||||
|
||||
def _construct_form(self, i, **kwargs):
|
||||
kwargs['event'] = self.event
|
||||
return super()._construct_form(i, **kwargs)
|
||||
|
||||
@property
|
||||
def empty_form(self):
|
||||
form = self.form(
|
||||
auto_id=self.auto_id,
|
||||
prefix=self.add_prefix('__prefix__'),
|
||||
empty_permitted=True,
|
||||
event=self.event,
|
||||
)
|
||||
self.add_fields(form, None)
|
||||
return form
|
||||
|
||||
@@ -9,7 +9,9 @@ from django.utils.formats import date_format
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.base.models import Event, ItemVariation, LogEntry, OrderPosition
|
||||
from pretix.base.models import (
|
||||
CheckinList, Event, ItemVariation, LogEntry, OrderPosition,
|
||||
)
|
||||
from pretix.base.signals import logentry_display
|
||||
|
||||
OVERVIEW_BLACKLIST = [
|
||||
@@ -167,6 +169,9 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'pretix.event.taxrule.added': _('The tax rule has been added.'),
|
||||
'pretix.event.taxrule.deleted': _('The tax rule has been deleted.'),
|
||||
'pretix.event.taxrule.changed': _('The tax rule has been changed.'),
|
||||
'pretix.event.checkinlist.added': _('The check-in list has been added.'),
|
||||
'pretix.event.checkinlist.deleted': _('The check-in list has been deleted.'),
|
||||
'pretix.event.checkinlist.changed': _('The check-in list has been changed.'),
|
||||
'pretix.event.settings': _('The event settings have been changed.'),
|
||||
'pretix.event.tickets.settings': _('The ticket download settings have been changed.'),
|
||||
'pretix.event.plugins.enabled': _('A plugin has been enabled.'),
|
||||
@@ -222,15 +227,24 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
dt = dateutil.parser.parse(data.get('datetime'))
|
||||
tz = pytz.timezone(sender.settings.timezone)
|
||||
dt_formatted = date_format(dt.astimezone(tz), "SHORT_DATETIME_FORMAT")
|
||||
if 'list' in data:
|
||||
try:
|
||||
checkin_list = sender.checkin_lists.get(pk=data.get('list')).name
|
||||
except CheckinList.DoesNotExist:
|
||||
checkin_list = _("(unknown)")
|
||||
else:
|
||||
checkin_list = _("(unknown)")
|
||||
|
||||
if data.get('first'):
|
||||
return _('Position #{posid} has been checked in manually at {datetime}.').format(
|
||||
return _('Position #{posid} has been checked in manually at {datetime} on list "{list}".').format(
|
||||
posid=data.get('positionid'),
|
||||
datetime=dt_formatted
|
||||
datetime=dt_formatted,
|
||||
list=checkin_list,
|
||||
)
|
||||
return _('Position #{posid} has been checked in again at {datetime}.').format(
|
||||
return _('Position #{posid} has been checked in again at {datetime} on list "{list}".').format(
|
||||
posid=data.get('positionid'),
|
||||
datetime=dt_formatted
|
||||
datetime=dt_formatted,
|
||||
list=checkin_list
|
||||
)
|
||||
|
||||
if logentry.action_type == 'pretix.team.member.added':
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
125
src/pretix/control/templates/pretixcontrol/checkin/lists.html
Normal file
125
src/pretix/control/templates/pretixcontrol/checkin/lists.html
Normal 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 %}
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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:" %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -164,7 +164,14 @@ urlpatterns = [
|
||||
url(r'^orders/$', orders.OrderList.as_view(), name='event.orders'),
|
||||
url(r'^waitinglist/$', waitinglist.WaitingListView.as_view(), name='event.orders.waitinglist'),
|
||||
url(r'^waitinglist/auto_assign$', waitinglist.AutoAssign.as_view(), name='event.orders.waitinglist.auto'),
|
||||
url(r'^waitinglist/(?P<entry>\d+)/delete$', waitinglist.EntryDelete.as_view(), name='event.orders.waitinglist.delete'),
|
||||
url(r'^checkins/$', checkin.CheckInView.as_view(), name='event.orders.checkins'),
|
||||
url(r'^waitinglist/(?P<entry>\d+)/delete$', waitinglist.EntryDelete.as_view(),
|
||||
name='event.orders.waitinglist.delete'),
|
||||
url(r'^checkinlists/$', checkin.CheckinListList.as_view(), name='event.orders.checkinlists'),
|
||||
url(r'^checkinlists/add$', checkin.CheckinListCreate.as_view(), name='event.orders.checkinlists.add'),
|
||||
url(r'^checkinlists/(?P<list>\d+)/$', checkin.CheckInListShow.as_view(), name='event.orders.checkinlists.show'),
|
||||
url(r'^checkinlists/(?P<list>\d+)/change$', checkin.CheckinListUpdate.as_view(),
|
||||
name='event.orders.checkinlists.edit'),
|
||||
url(r'^checkinlists/(?P<list>\d+)/delete$', checkin.CheckinListDelete.as_view(),
|
||||
name='event.orders.checkinlists.delete'),
|
||||
])),
|
||||
]
|
||||
|
||||
@@ -1,122 +1,221 @@
|
||||
import dateutil.parser
|
||||
from django.contrib import messages
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db.models import F, Prefetch, Q
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.shortcuts import redirect
|
||||
from django.utils.timezone import now
|
||||
from django.db import transaction
|
||||
from django.db.models import Max, OuterRef, Subquery
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.generic import ListView
|
||||
from django.views.generic import DeleteView, ListView
|
||||
from pytz import UTC
|
||||
|
||||
from pretix.base.models import Checkin, Item, Order, OrderPosition
|
||||
from pretix.base.models import Checkin, Order, OrderPosition
|
||||
from pretix.base.models.checkin import CheckinList
|
||||
from pretix.control.forms.checkin import CheckinListForm
|
||||
from pretix.control.forms.filter import CheckInFilterForm
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.control.views import CreateView, UpdateView
|
||||
|
||||
|
||||
class CheckInView(EventPermissionRequiredMixin, ListView):
|
||||
class CheckInListShow(EventPermissionRequiredMixin, ListView):
|
||||
model = Checkin
|
||||
context_object_name = 'entries'
|
||||
paginate_by = 30
|
||||
template_name = 'pretixcontrol/checkin/index.html'
|
||||
permission = 'can_view_orders'
|
||||
|
||||
def get_queryset(self):
|
||||
def get_queryset(self, filter=True):
|
||||
cqs = Checkin.objects.filter(
|
||||
position_id=OuterRef('pk'),
|
||||
list_id=self.list.pk
|
||||
).order_by().values('position_id').annotate(
|
||||
m=Max('datetime')
|
||||
).values('m')
|
||||
|
||||
qs = OrderPosition.objects.filter(order__event=self.request.event, order__status='p')
|
||||
qs = OrderPosition.objects.filter(
|
||||
order__event=self.request.event,
|
||||
order__status=Order.STATUS_PAID,
|
||||
subevent=self.list.subevent
|
||||
).annotate(
|
||||
last_checked_in=Subquery(cqs)
|
||||
).select_related('item', 'variation', 'order', 'addon_to')
|
||||
|
||||
# if this setting is False, we check only items for admission
|
||||
if not self.request.event.settings.ticket_download_nonadm:
|
||||
qs = qs.filter(item__admission=True)
|
||||
if not self.list.all_products:
|
||||
qs = qs.filter(item__in=self.list.limit_products.values_list('id', flat=True))
|
||||
|
||||
if self.request.GET.get("status", "") != "":
|
||||
p = self.request.GET.get("status", "")
|
||||
if p == '1':
|
||||
# records with check-in record
|
||||
qs = qs.filter(checkins__isnull=False)
|
||||
elif p == '0':
|
||||
qs = qs.filter(checkins__isnull=True)
|
||||
if filter and self.filter_form.is_valid():
|
||||
qs = self.filter_form.filter_qs(qs)
|
||||
|
||||
if self.request.GET.get("user", "") != "":
|
||||
u = self.request.GET.get("user", "")
|
||||
qs = qs.filter(
|
||||
Q(order__email__icontains=u) | Q(attendee_name__icontains=u) | Q(attendee_email__icontains=u)
|
||||
)
|
||||
return qs
|
||||
|
||||
if self.request.GET.get("item", "") != "":
|
||||
u = self.request.GET.get("item", "")
|
||||
qs = qs.filter(item_id=u)
|
||||
@cached_property
|
||||
def filter_form(self):
|
||||
return CheckInFilterForm(
|
||||
data=self.request.GET,
|
||||
event=self.request.event,
|
||||
list=self.list
|
||||
)
|
||||
|
||||
if self.request.GET.get("subevent", "") != "":
|
||||
s = self.request.GET.get("subevent", "")
|
||||
qs = qs.filter(subevent_id=s)
|
||||
|
||||
qs = qs.prefetch_related(
|
||||
Prefetch('checkins', queryset=Checkin.objects.filter(position__order__event=self.request.event))
|
||||
).select_related('order', 'item', 'addon_to')
|
||||
|
||||
if self.request.GET.get("ordering", "") != "":
|
||||
p = self.request.GET.get("ordering", "")
|
||||
keys_allowed = self.get_ordering_keys_mappings()
|
||||
if p in keys_allowed:
|
||||
mapped_field = keys_allowed[p]
|
||||
if isinstance(mapped_field, dict):
|
||||
order = mapped_field.pop('_order')
|
||||
qs = qs.annotate(**mapped_field).order_by(order)
|
||||
elif isinstance(mapped_field, (list, tuple)):
|
||||
qs = qs.order_by(*mapped_field)
|
||||
else:
|
||||
qs = qs.order_by(mapped_field)
|
||||
|
||||
return qs.distinct()
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.list = get_object_or_404(self.request.event.checkin_lists.all(), pk=kwargs.get("list"))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['items'] = Item.objects.filter(event=self.request.event)
|
||||
ctx['filtered'] = ("status" in self.request.GET or "user" in self.request.GET or "item" in self.request.GET
|
||||
or "subevent" in self.request.GET)
|
||||
ctx['checkinlist'] = self.list
|
||||
ctx['filter_form'] = self.filter_form
|
||||
for e in ctx['entries']:
|
||||
if e.last_checked_in:
|
||||
if isinstance(e.last_checked_in, str):
|
||||
# Apparently only happens on SQLite
|
||||
e.last_checked_in_aware = make_aware(dateutil.parser.parse(e.last_checked_in), UTC)
|
||||
else:
|
||||
e.last_checked_in_aware = e.last_checked_in
|
||||
return ctx
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
positions = OrderPosition.objects.select_related('item', 'variation', 'order', 'addon_to').filter(
|
||||
order__event=self.request.event,
|
||||
if "can_change_orders" not in request.eventpermset:
|
||||
messages.error(request, _('You do not have permission to perform this action.'))
|
||||
return redirect(reverse('control:event.orders.checkins', kwargs={
|
||||
'event': self.request.event.slug,
|
||||
'organizer': self.request.event.organizer.slug
|
||||
}) + '?' + request.GET.urlencode())
|
||||
|
||||
positions = self.get_queryset(filter=False).filter(
|
||||
pk__in=request.POST.getlist('checkin')
|
||||
)
|
||||
|
||||
for op in positions:
|
||||
created = False
|
||||
if op.order.status == Order.STATUS_PAID:
|
||||
ci, created = Checkin.objects.get_or_create(position=op, defaults={
|
||||
ci, created = Checkin.objects.get_or_create(position=op, list=self.list, defaults={
|
||||
'datetime': now(),
|
||||
})
|
||||
op.order.log_action('pretix.control.views.checkin', data={
|
||||
'position': op.id,
|
||||
'positionid': op.positionid,
|
||||
'first': created,
|
||||
'datetime': now()
|
||||
'datetime': now(),
|
||||
'list': self.list.pk
|
||||
}, user=request.user)
|
||||
|
||||
messages.success(request, _('The selected tickets have been marked as checked in.'))
|
||||
return redirect(reverse('control:event.orders.checkins', kwargs={
|
||||
return redirect(reverse('control:event.orders.checkinlists.show', kwargs={
|
||||
'event': self.request.event.slug,
|
||||
'organizer': self.request.event.organizer.slug
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'list': self.list.pk
|
||||
}) + '?' + request.GET.urlencode())
|
||||
|
||||
@staticmethod
|
||||
def get_ordering_keys_mappings():
|
||||
return {
|
||||
'code': 'order__code',
|
||||
'-code': '-order__code',
|
||||
'email': 'order__email',
|
||||
'-email': '-order__email',
|
||||
# Set nulls_first to be consistent over databases
|
||||
'status': F('checkins__id').asc(nulls_first=True),
|
||||
'-status': F('checkins__id').desc(nulls_last=True),
|
||||
'timestamp': F('checkins__datetime').asc(nulls_first=True),
|
||||
'-timestamp': F('checkins__datetime').desc(nulls_last=True),
|
||||
'item': ('item__name', 'variation__value'),
|
||||
'-item': ('-item__name', 'variation__value'),
|
||||
'subevent': ('subevent__date_from', 'subevent__name'),
|
||||
'-subevent': ('-subevent__date_from', '-subevent__name'),
|
||||
'name': {'_order': F('display_name').asc(nulls_first=True),
|
||||
'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')},
|
||||
'-name': {'_order': F('display_name').desc(nulls_last=True),
|
||||
'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')},
|
||||
}
|
||||
|
||||
class CheckinListList(EventPermissionRequiredMixin, ListView):
|
||||
model = CheckinList
|
||||
context_object_name = 'checkinlists'
|
||||
paginate_by = 30
|
||||
permission = 'can_view_orders'
|
||||
template_name = 'pretixcontrol/checkin/lists.html'
|
||||
|
||||
def get_queryset(self):
|
||||
qs = self.request.event.checkin_lists.prefetch_related("limit_products")
|
||||
qs = CheckinList.annotate_with_numbers(qs, self.request.event)
|
||||
|
||||
if self.request.GET.get("subevent", "") != "":
|
||||
s = self.request.GET.get("subevent", "")
|
||||
qs = qs.filter(subevent_id=s)
|
||||
return qs
|
||||
|
||||
|
||||
class CheckinListCreate(EventPermissionRequiredMixin, CreateView):
|
||||
model = CheckinList
|
||||
form_class = CheckinListForm
|
||||
template_name = 'pretixcontrol/checkin/list_edit.html'
|
||||
permission = 'can_change_event_settings'
|
||||
context_object_name = 'checkinlist'
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.orders.checkinlists', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
})
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
form.instance.event = self.request.event
|
||||
messages.success(self.request, _('The new check-in list has been created.'))
|
||||
ret = super().form_valid(form)
|
||||
form.instance.log_action('pretix.event.checkinlist.added', user=self.request.user,
|
||||
data=dict(form.cleaned_data))
|
||||
return ret
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.error(self.request, _('We could not save your changes. See below for details.'))
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class CheckinListUpdate(EventPermissionRequiredMixin, UpdateView):
|
||||
model = CheckinList
|
||||
form_class = CheckinListForm
|
||||
template_name = 'pretixcontrol/checkin/list_edit.html'
|
||||
permission = 'can_change_event_settings'
|
||||
context_object_name = 'checkinlist'
|
||||
|
||||
def get_object(self, queryset=None) -> CheckinList:
|
||||
try:
|
||||
return self.request.event.checkin_lists.get(
|
||||
id=self.kwargs['list']
|
||||
)
|
||||
except CheckinList.DoesNotExist:
|
||||
raise Http404(_("The requested list does not exist."))
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
if form.has_changed():
|
||||
self.object.log_action(
|
||||
'pretix.event.checkinlist.changed', user=self.request.user, data={
|
||||
k: form.cleaned_data.get(k) for k in form.changed_data
|
||||
}
|
||||
)
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.orders.checkinlists.show', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
'list': self.object.pk
|
||||
})
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.error(self.request, _('We could not save your changes. See below for details.'))
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class CheckinListDelete(EventPermissionRequiredMixin, DeleteView):
|
||||
model = CheckinList
|
||||
template_name = 'pretixcontrol/checkin/list_delete.html'
|
||||
permission = 'can_change_event_settings'
|
||||
context_object_name = 'checkinlist'
|
||||
|
||||
def get_object(self, queryset=None) -> CheckinList:
|
||||
try:
|
||||
return self.request.event.checkin_lists.get(
|
||||
id=self.kwargs['list']
|
||||
)
|
||||
except CheckinList.DoesNotExist:
|
||||
raise Http404(_("The requested list does not exist."))
|
||||
|
||||
@transaction.atomic
|
||||
def delete(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
success_url = self.get_success_url()
|
||||
self.object.log_action(action='pretix.event.orders.deleted', user=request.user)
|
||||
self.object.delete()
|
||||
messages.success(self.request, _('The selected list has been deleted.'))
|
||||
return HttpResponseRedirect(success_url)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.orders.checkinlists', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
})
|
||||
|
||||
@@ -20,6 +20,7 @@ from pretix.base.models import (
|
||||
Event, Item, Order, OrderPosition, RequiredAction, SubEvent, Voucher,
|
||||
WaitingListEntry,
|
||||
)
|
||||
from pretix.base.models.checkin import CheckinList
|
||||
from pretix.control.forms.event import CommentForm
|
||||
from pretix.control.signals import (
|
||||
event_dashboard_widgets, user_dashboard_widgets,
|
||||
@@ -190,7 +191,7 @@ def shop_state_widget(sender, **kwargs):
|
||||
|
||||
|
||||
@receiver(signal=event_dashboard_widgets)
|
||||
def checkin_widget(sender, **kwargs):
|
||||
def checkin_widget(sender, subevent=None, **kwargs):
|
||||
size_qs = OrderPosition.objects.filter(order__event=sender, order__status='p')
|
||||
checked_qs = OrderPosition.objects.filter(order__event=sender, order__status='p', checkins__isnull=False)
|
||||
|
||||
@@ -199,15 +200,22 @@ def checkin_widget(sender, **kwargs):
|
||||
size_qs = size_qs.filter(item__admission=True)
|
||||
checked_qs = checked_qs.filter(item__admission=True)
|
||||
|
||||
return [{
|
||||
'content': NUM_WIDGET.format(num='{}/{}'.format(checked_qs.count(), size_qs.count()), text=_('Checked in')),
|
||||
'display_size': 'small',
|
||||
'priority': 50,
|
||||
'url': reverse('control:event.orders.checkins', kwargs={
|
||||
'event': sender.slug,
|
||||
'organizer': sender.organizer.slug
|
||||
widgets = []
|
||||
qs = sender.checkin_lists.filter(subevent=subevent)
|
||||
qs = CheckinList.annotate_with_numbers(qs, sender)
|
||||
for cl in qs:
|
||||
widgets.append({
|
||||
'content': NUM_WIDGET.format(num='{}/{}'.format(cl.checkin_count, cl.position_count),
|
||||
text=_('Checked in – {list}').format(list=escape(cl.name))),
|
||||
'display_size': 'small',
|
||||
'priority': 50,
|
||||
'url': reverse('control:event.orders.checkinlists.show', kwargs={
|
||||
'event': sender.slug,
|
||||
'organizer': sender.organizer.slug,
|
||||
'list': cl.pk
|
||||
})
|
||||
})
|
||||
}]
|
||||
return widgets
|
||||
|
||||
|
||||
@receiver(signal=event_dashboard_widgets)
|
||||
|
||||
@@ -15,6 +15,7 @@ from django.views.generic import ListView
|
||||
from formtools.wizard.views import SessionWizardView
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import Event, Organizer, Quota, Team
|
||||
from pretix.control.forms.event import (
|
||||
EventWizardBasicsForm, EventWizardCopyForm, EventWizardFoundationForm,
|
||||
@@ -143,7 +144,7 @@ class EventWizard(SessionWizardView):
|
||||
basics_data = self.get_cleaned_data_for_step('basics')
|
||||
copy_data = self.get_cleaned_data_for_step('copy')
|
||||
|
||||
with transaction.atomic():
|
||||
with transaction.atomic(), language(basics_data['locale']):
|
||||
event = form_dict['basics'].instance
|
||||
event.organizer = foundation_data['organizer']
|
||||
event.plugins = settings.PRETIX_PLUGINS_DEFAULT
|
||||
@@ -165,7 +166,7 @@ class EventWizard(SessionWizardView):
|
||||
t.limit_events.add(event)
|
||||
|
||||
if event.has_subevents:
|
||||
event.subevents.create(
|
||||
se = event.subevents.create(
|
||||
name=event.name,
|
||||
date_from=event.date_from,
|
||||
date_to=event.date_to,
|
||||
@@ -191,6 +192,17 @@ class EventWizard(SessionWizardView):
|
||||
if copy_data and copy_data['copy_from_event']:
|
||||
from_event = copy_data['copy_from_event']
|
||||
event.copy_data_from(from_event)
|
||||
elif event.has_subevents:
|
||||
event.checkin_lists.create(
|
||||
name=str(se),
|
||||
all_products=True,
|
||||
subevent=se
|
||||
)
|
||||
else:
|
||||
event.checkin_lists.create(
|
||||
name=_('Default'),
|
||||
all_products=True
|
||||
)
|
||||
|
||||
event.settings.set('timezone', basics_data['timezone'])
|
||||
event.settings.set('locale', basics_data['locale'])
|
||||
|
||||
@@ -147,7 +147,7 @@ class OrderDetail(OrderView):
|
||||
).select_related(
|
||||
'item', 'variation', 'addon_to', 'tax_rule'
|
||||
).prefetch_related(
|
||||
'item__questions', 'answers', 'answers__question', 'checkins'
|
||||
'item__questions', 'answers', 'answers__question', 'checkins', 'checkins__list'
|
||||
).order_by('positionid')
|
||||
|
||||
positions = []
|
||||
@@ -906,9 +906,21 @@ class ExportMixin:
|
||||
responses = register_data_exporters.send(self.request.event)
|
||||
for receiver, response in responses:
|
||||
ex = response(self.request.event)
|
||||
if self.request.GET.get("identifier") and ex.identifier != self.request.GET.get("identifier"):
|
||||
continue
|
||||
|
||||
# Use form parse cycle to generate useful defaults
|
||||
test_form = ExporterForm(data=self.request.GET, prefix=ex.identifier)
|
||||
test_form.fields = ex.export_form_fields
|
||||
test_form.is_valid()
|
||||
initial = {
|
||||
k: v for k, v in test_form.cleaned_data.items() if ex.identifier + "-" + k in self.request.GET
|
||||
}
|
||||
|
||||
ex.form = ExporterForm(
|
||||
data=(self.request.POST if self.request.method == 'POST' else None),
|
||||
prefix=ex.identifier
|
||||
prefix=ex.identifier,
|
||||
initial=initial
|
||||
)
|
||||
ex.form.fields = ex.export_form_fields
|
||||
exporters.append(ex)
|
||||
|
||||
@@ -11,13 +11,15 @@ from django.utils.functional import cached_property
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from django.views.generic import CreateView, DeleteView, ListView, UpdateView
|
||||
|
||||
from pretix.base.models.checkin import CheckinList
|
||||
from pretix.base.models.event import SubEvent, SubEventMetaValue
|
||||
from pretix.base.models.items import Quota, SubEventItem, SubEventItemVariation
|
||||
from pretix.control.forms.checkin import CheckinListForm
|
||||
from pretix.control.forms.filter import SubEventFilterForm
|
||||
from pretix.control.forms.item import QuotaForm
|
||||
from pretix.control.forms.subevents import (
|
||||
QuotaFormSet, SubEventForm, SubEventItemForm, SubEventItemVariationForm,
|
||||
SubEventMetaValueForm,
|
||||
CheckinListFormSet, QuotaFormSet, SubEventForm, SubEventItemForm,
|
||||
SubEventItemVariationForm, SubEventMetaValueForm,
|
||||
)
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.control.views.event import MetaDataEditorMixin
|
||||
@@ -132,6 +134,41 @@ class SubEventEditorMixin(MetaDataEditorMixin):
|
||||
data=(self.request.POST if self.request.method == "POST" else None)
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def cl_formset(self):
|
||||
extra = 0
|
||||
kwargs = {}
|
||||
|
||||
if self.copy_from:
|
||||
kwargs['initial'] = [
|
||||
{
|
||||
'name': cl.name,
|
||||
'all_products': cl.all_products,
|
||||
'limit_products': cl.limit_products.all(),
|
||||
} for cl in self.copy_from.checkinlist_set.prefetch_related('limit_products')
|
||||
]
|
||||
extra = len(kwargs['initial'])
|
||||
elif not self.object:
|
||||
kwargs['initial'] = [
|
||||
{
|
||||
'name': '',
|
||||
'all_products': True,
|
||||
}
|
||||
]
|
||||
extra = 1
|
||||
|
||||
formsetclass = inlineformset_factory(
|
||||
SubEvent, CheckinList,
|
||||
form=CheckinListForm, formset=CheckinListFormSet,
|
||||
can_order=False, can_delete=True, extra=extra,
|
||||
)
|
||||
if self.object:
|
||||
kwargs['queryset'] = self.object.checkinlist_set.prefetch_related('limit_products')
|
||||
|
||||
return formsetclass(self.request.POST if self.request.method == "POST" else None,
|
||||
instance=self.object,
|
||||
event=self.request.event, **kwargs)
|
||||
|
||||
@cached_property
|
||||
def formset(self):
|
||||
extra = 0
|
||||
@@ -161,6 +198,38 @@ class SubEventEditorMixin(MetaDataEditorMixin):
|
||||
instance=self.object,
|
||||
event=self.request.event, **kwargs)
|
||||
|
||||
def save_cl_formset(self, obj):
|
||||
for form in self.cl_formset.initial_forms:
|
||||
if form in self.cl_formset.deleted_forms:
|
||||
if not form.instance.pk:
|
||||
continue
|
||||
form.instance.log_action(action='pretix.event.checkinlist.deleted', user=self.request.user)
|
||||
form.instance.delete()
|
||||
form.instance.pk = None
|
||||
elif form.has_changed():
|
||||
form.instance.subevent = obj
|
||||
form.instance.event = obj.event
|
||||
form.save()
|
||||
change_data = {k: form.cleaned_data.get(k) for k in form.changed_data}
|
||||
change_data['id'] = form.instance.pk
|
||||
form.instance.log_action(
|
||||
'pretix.event.checkinlist.changed', user=self.request.user, data={
|
||||
k: form.cleaned_data.get(k) for k in form.changed_data
|
||||
}
|
||||
)
|
||||
|
||||
for form in self.cl_formset.extra_forms:
|
||||
if not form.has_changed():
|
||||
continue
|
||||
if self.formset._should_delete_form(form):
|
||||
continue
|
||||
form.instance.subevent = obj
|
||||
form.instance.event = obj.event
|
||||
form.save()
|
||||
change_data = {k: form.cleaned_data.get(k) for k in form.changed_data}
|
||||
change_data['id'] = form.instance.pk
|
||||
form.instance.log_action(action='pretix.event.checkinlist.added', user=self.request.user, data=change_data)
|
||||
|
||||
def save_formset(self, obj):
|
||||
for form in self.formset.initial_forms:
|
||||
if form in self.formset.deleted_forms:
|
||||
@@ -204,6 +273,7 @@ class SubEventEditorMixin(MetaDataEditorMixin):
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['formset'] = self.formset
|
||||
ctx['cl_formset'] = self.cl_formset
|
||||
ctx['itemvar_forms'] = self.itemvar_forms
|
||||
ctx['meta_forms'] = self.meta_forms
|
||||
return ctx
|
||||
@@ -259,7 +329,7 @@ class SubEventEditorMixin(MetaDataEditorMixin):
|
||||
def is_valid(self, form):
|
||||
return form.is_valid() and all([f.is_valid() for f in self.itemvar_forms]) and self.formset.is_valid() and (
|
||||
all([f.is_valid() for f in self.meta_forms])
|
||||
)
|
||||
) and self.cl_formset.is_valid()
|
||||
|
||||
|
||||
class SubEventUpdate(EventPermissionRequiredMixin, SubEventEditorMixin, UpdateView):
|
||||
@@ -288,6 +358,7 @@ class SubEventUpdate(EventPermissionRequiredMixin, SubEventEditorMixin, UpdateVi
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
self.save_formset(self.object)
|
||||
self.save_cl_formset(self.object)
|
||||
self.save_meta()
|
||||
|
||||
for f in self.itemvar_forms:
|
||||
@@ -355,6 +426,7 @@ class SubEventCreate(SubEventEditorMixin, EventPermissionRequiredMixin, CreateVi
|
||||
form.instance.log_action('pretix.subevent.added', data=dict(form.cleaned_data), user=self.request.user)
|
||||
|
||||
self.save_formset(form.instance)
|
||||
self.save_cl_formset(form.instance)
|
||||
for f in self.itemvar_forms:
|
||||
f.instance.subevent = form.instance
|
||||
f.save()
|
||||
|
||||
Reference in New Issue
Block a user