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

* Data model and migration

* Some backwards compatibility

* CRUD for checkin lists

* Show and perform checkins

* Correct numbers in table and dashboard widget

* event creation and cloning

* Allow to link specific exports and pass options per query

* Play with the CSV export

* PDF export

* Collapse exports by default

* Improve PDF exporter

* Addon stuff

* Subevent stuff, pretixdroid tests

* pretixdroid tests

* Add CRUD API

* Test compatibility

* Fix test

* DB-independent sorting behavior

* Add CRUD and coyp tests

* Re-enable pretixdroid plugin

* pretixdroid config

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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