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,37 @@
from django.utils.translation import ugettext as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import CheckinList
class CheckinListSerializer(I18nAwareModelSerializer):
checkin_count = serializers.IntegerField(read_only=True)
position_count = serializers.IntegerField(read_only=True)
class Meta:
model = CheckinList
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count')
def validate(self, data):
data = super().validate(data)
event = self.context['event']
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
full_data.update(data)
for item in full_data.get('limit_products'):
if event != item.event:
raise ValidationError(_('One or more items do not belong to this event.'))
if event.has_subevents:
if not full_data.get('subevent'):
raise ValidationError(_('Subevent cannot be null for event series.'))
if event != full_data.get('subevent').event:
raise ValidationError(_('The subevent does not belong to this event.'))
else:
if full_data.get('subevent'):
raise ValidationError(_('The subevent does not belong to this event.'))
return data

View File

@@ -1,19 +0,0 @@
{% extends "rest_framework/base.html" %}
{% load staticfiles %}
{% load compress %}
{% block bootstrap_theme %}
{% compress css %}
<link rel="stylesheet" type="text/x-scss" href="{% static "rest_framework/scss/main.scss" %}" />
{% endcompress %}
{% endblock %}
{% block branding %}
<a class="navbar-brand" href="/api/v1/">pretix REST API</a>
{% endblock %}
{% block description %}
<div class="alert alert-info alert-docs-link">
<a href="https://docs.pretix.eu/en/latest/api/index.html">
You can find documentation on our REST API on docs.pretix.eu.
</a>
</div>
{% endblock %}

View File

@@ -4,7 +4,7 @@ from django.apps import apps
from django.conf.urls import include, url
from rest_framework import routers
from .views import event, item, order, organizer, voucher, waitinglist
from .views import checkin, event, item, order, organizer, voucher, waitinglist
router = routers.DefaultRouter()
router.register(r'organizers', organizer.OrganizerViewSet)
@@ -24,6 +24,7 @@ event_router.register(r'orderpositions', order.OrderPositionViewSet)
event_router.register(r'invoices', order.InvoiceViewSet)
event_router.register(r'taxrules', event.TaxRuleViewSet)
event_router.register(r'waitinglistentries', waitinglist.WaitingListViewSet)
event_router.register(r'checkinlists', checkin.CheckinListViewSet)
# Force import of all plugins to give them a chance to register URLs with the router
for app in apps.get_app_configs():

View File

@@ -0,0 +1,59 @@
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from rest_framework import viewsets
from pretix.api.serializers.checkin import CheckinListSerializer
from pretix.base.models import CheckinList
from pretix.base.models.organizer import TeamAPIToken
class CheckinListFilter(FilterSet):
class Meta:
model = CheckinList
fields = ['subevent']
class CheckinListViewSet(viewsets.ModelViewSet):
serializer_class = CheckinListSerializer
queryset = CheckinList.objects.none()
filter_backends = (DjangoFilterBackend,)
filter_class = CheckinListFilter
permission = 'can_view_orders'
write_permission = 'can_change_event_settings'
def get_queryset(self):
qs = self.request.event.checkin_lists.prefetch_related(
'limit_products',
)
qs = CheckinList.annotate_with_numbers(qs, self.request.event)
return qs
def perform_create(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
'pretix.event.checkinlist.added',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
return ctx
def perform_update(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
'pretix.event.checkinlist.changed',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
def perform_destroy(self, instance):
instance.log_action(
'pretix.event.checkinlist.deleted',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
)
super().perform_destroy(instance)

View File

@@ -0,0 +1,106 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-11-24 16:29
from __future__ import unicode_literals
import django.core.validators
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
from django.utils.translation import ugettext as _
import pretix.base.validators
from pretix.base.i18n import language
def create_checkin_lists(apps, schema_editor):
Event = apps.get_model('pretixbase', 'Event')
Checkin = apps.get_model('pretixbase', 'Checkin')
EventSettingsStore = apps.get_model('pretixbase', 'Event_SettingsStore')
for e in Event.objects.all():
locale = EventSettingsStore.objects.filter(object=e, key='locale').first()
if locale:
locale = locale.value
else:
locale = settings.LANGUAGE_CODE
if e.has_subevents:
for se in e.subevents.all():
with language(locale):
cl = e.checkin_lists.create(name=se.name, subevent=se, all_products=True)
Checkin.objects.filter(position__subevent=se, position__order__event=e).update(list=cl)
else:
with language(locale):
cl = e.checkin_lists.create(name=_('Default list'), all_products=True)
Checkin.objects.filter(position__order__event=e).update(list=cl)
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0076_orderfee_squashed_0082_invoiceaddress_internal_reference'),
]
operations = [
migrations.AlterField(
model_name='event',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes, and must be unique among your events. We recommend some kind of abbreviation or a date with less than 10 characters that can be easily remembered, but you can also choose to use a random value. This will be used in URLs, order codes, invoice numbers, and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBlacklistValidator()], verbose_name='Short form'),
),
migrations.AlterField(
model_name='eventmetaproperty',
name='name',
field=models.CharField(db_index=True, help_text='Can not contain spaces or special characters except underscores', max_length=50, validators=[django.core.validators.RegexValidator(message='The property name may only contain letters, numbers and underscores.', regex='^[a-zA-Z0-9_]+$')], verbose_name='Name'),
),
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used once. This is being used in URLs to refer to your organizer accounts and your events.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Short form'),
),
migrations.CreateModel(
name='CheckinList',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=190)),
('all_products', models.BooleanField(default=True, verbose_name='All products (including newly created ones)')),
],
),
migrations.AddField(
model_name='checkinlist',
name='event',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='checkin_lists', to='pretixbase.Event'),
),
migrations.AddField(
model_name='checkinlist',
name='subevent',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, null=True, blank=True, related_name='checkin_lists', to='pretixbase.SubEvent'),
),
migrations.AddField(
model_name='checkinlist',
name='limit_products',
field=models.ManyToManyField(blank=True, to='pretixbase.Item', verbose_name='Limit to products'),
),
migrations.AddField(
model_name='checkin',
name='list',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='checkins', to='pretixbase.CheckinList'),
),
migrations.AlterField(
model_name='checkin',
name='list',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='checkins', to='pretixbase.CheckinList'),
),
migrations.AlterField(
model_name='checkinlist',
name='subevent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent', verbose_name='Date'),
),
migrations.RunPython(
create_checkin_lists,
migrations.RunPython.noop
),
migrations.AlterField(
model_name='checkin',
name='list',
field=models.ForeignKey(null=False, on_delete=django.db.models.deletion.PROTECT, related_name='checkins', to='pretixbase.CheckinList'),
),
]

View File

@@ -1,7 +1,7 @@
from ..settings import GlobalSettingsObject_SettingsStore
from .auth import U2FDevice, User
from .base import CachedFile, LoggedModel, cachedfile_name
from .checkin import Checkin
from .checkin import Checkin, CheckinList
from .event import (
Event, Event_SettingsStore, EventLock, EventMetaProperty, EventMetaValue,
RequiredAction, SubEvent, SubEventMetaValue, generate_invite_token,

View File

@@ -1,5 +1,76 @@
from django.db import models
from django.db.models import Case, Count, F, OuterRef, Q, Subquery, When
from django.db.models.functions import Coalesce
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from pretix.base.models import LoggedModel
class CheckinList(LoggedModel):
event = models.ForeignKey('Event', related_name='checkin_lists')
name = models.CharField(max_length=190)
all_products = models.BooleanField(default=True, verbose_name=_("All products (including newly created ones)"))
limit_products = models.ManyToManyField('Item', verbose_name=_("Limit to products"), blank=True)
subevent = models.ForeignKey('SubEvent', null=True, blank=True,
verbose_name=pgettext_lazy('subevent', 'Date'))
@staticmethod
def annotate_with_numbers(qs, event):
from . import Order, OrderPosition
cqs = Checkin.objects.filter(
position__order__event=event,
position__order__status=Order.STATUS_PAID,
list=OuterRef('pk')
).filter(
# This assumes that in an event with subevents, *all* positions have subevents
# and *all* checkin lists have a subevent assigned
Q(position__subevent=OuterRef('subevent'))
| (Q(position__subevent__isnull=True))
).order_by().values('list').annotate(
c=Count('*')
).values('c')
pqs_all = OrderPosition.objects.filter(
order__event=event,
order__status=Order.STATUS_PAID,
).filter(
# This assumes that in an event with subevents, *all* positions have subevents
# and *all* checkin lists have a subevent assigned
Q(subevent=OuterRef('subevent'))
| (Q(subevent__isnull=True))
).order_by().values('order__event').annotate(
c=Count('*')
).values('c')
pqs_limited = OrderPosition.objects.filter(
order__event=event,
order__status=Order.STATUS_PAID,
item__in=OuterRef('limit_products')
).filter(
# This assumes that in an event with subevents, *all* positions have subevents
# and *all* checkin lists have a subevent assigned
Q(subevent=OuterRef('subevent'))
| (Q(subevent__isnull=True))
).order_by().values('order__event').annotate(
c=Count('*')
).values('c')
return qs.annotate(
checkin_count=Coalesce(Subquery(cqs, output_field=models.IntegerField()), 0),
position_count=Coalesce(Case(
When(all_products=True, then=Subquery(pqs_all, output_field=models.IntegerField())),
default=Subquery(pqs_limited, output_field=models.IntegerField()),
output_field=models.IntegerField()
), 0)
).annotate(
percent=Case(
When(position_count__gt=0, then=F('checkin_count') * 100 / F('position_count')),
default=0,
output_field=models.IntegerField()
)
)
def __str__(self):
return self.name
class Checkin(models.Model):
@@ -9,3 +80,11 @@ class Checkin(models.Model):
position = models.ForeignKey('pretixbase.OrderPosition', related_name='checkins')
datetime = models.DateTimeField(default=now)
nonce = models.CharField(max_length=190, null=True, blank=True)
list = models.ForeignKey(
'pretixbase.CheckinList', related_name='checkins', on_delete=models.PROTECT,
)
def __repr__(self):
return "<Checkin: pos {} on list '{}' at {}>".format(
self.position, self.list, self.datetime
)

View File

@@ -410,6 +410,14 @@ class Event(EventMixin, LoggedModel):
o.question = q
o.save()
for cl in other.checkin_lists.filter(subevent__isnull=True).prefetch_related('limit_products'):
items = list(cl.limit_products.all())
cl.pk = None
cl.event = self
cl.save()
for i in items:
cl.limit_products.add(item_map[i.pk])
for s in other.settings._objects.all():
s.object = self
s.pk = None

View File

@@ -1,6 +1,7 @@
from typing import Any, Dict
from django.core.files.base import ContentFile
from django.utils.timezone import override
from pretix.base.i18n import language
from pretix.base.models import CachedFile, Event, cachedfile_name
@@ -13,7 +14,7 @@ from pretix.celery_app import app
def export(event: str, fileid: str, provider: str, form_data: Dict[str, Any]) -> None:
event = Event.objects.get(id=event)
file = CachedFile.objects.get(id=fileid)
with language(event.settings.locale):
with language(event.settings.locale), override(event.settings.timezone):
responses = register_data_exporters.send(event)
for receiver, response in responses:
ex = response(event)

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

View File

@@ -1,6 +1,7 @@
import contextlib
from django.db import transaction
from django.db.models.expressions import OrderBy
class DummyRollbackException(Exception):
@@ -34,3 +35,25 @@ def casual_reads():
Kept for backwards compatibility.
"""
yield
class FixedOrderBy(OrderBy):
# Workaround for https://code.djangoproject.com/ticket/28848
template = '%(expression)s %(ordering)s'
def as_sql(self, compiler, connection, template=None, **extra_context):
if not template:
if self.nulls_last:
template = '%s NULLS LAST' % self.template
elif self.nulls_first:
template = '%s NULLS FIRST' % self.template
connection.ops.check_expression_support(self)
expression_sql, params = compiler.compile(self.expression)
placeholders = {
'expression': expression_sql,
'ordering': 'DESC' if self.descending else 'ASC',
}
placeholders.update(extra_context)
template = template or self.template
params = params * template.count('%(expression)s')
return (template % placeholders).rstrip(), params

View File

@@ -13,6 +13,7 @@ class CheckinlistsApp(AppConfig):
name = _("Check-in list exporter")
author = _("the pretix team")
version = version
visible = False
description = _("This plugin allows you to generate check-in lists for your conference.")
def ready(self):

View File

@@ -3,36 +3,31 @@ from collections import OrderedDict
from defusedcsv import csv
from django import forms
from django.db.models import Max, OuterRef, Subquery
from django.db.models.functions import Coalesce
from django.utils.translation import (
pgettext, pgettext_lazy, ugettext as _, ugettext_lazy,
)
from django.utils.formats import localize
from django.utils.translation import pgettext, ugettext as _, ugettext_lazy
from reportlab.lib.units import mm
from reportlab.platypus import Flowable, Paragraph, Spacer, Table, TableStyle
from pretix.base.exporter import BaseExporter
from pretix.base.models import Order, OrderPosition, Question
from pretix.base.models import Checkin, Order, OrderPosition, Question
from pretix.plugins.reports.exporters import ReportlabExportMixin
class BaseCheckinList(BaseExporter):
pass
class CSVCheckinList(BaseCheckinList):
name = "overview"
identifier = 'checkinlistcsv'
verbose_name = ugettext_lazy('Check-in list (CSV)')
@property
def export_form_fields(self):
d = OrderedDict(
[
('items',
forms.ModelMultipleChoiceField(
queryset=self.event.items.all(),
label=_('Limit to products'),
widget=forms.CheckboxSelectMultiple(
attrs={'class': 'scrolling-multiple-choice'}
('list',
forms.ModelChoiceField(
queryset=self.event.checkin_lists.all(),
label=_('Check-in list'),
widget=forms.RadioSelect(
attrs={'class': 'scrolling-choice'}
),
initial=self.event.items.filter(admission=True)
initial=self.event.checkin_lists.first()
)),
('secrets',
forms.BooleanField(
@@ -67,26 +62,191 @@ class CSVCheckinList(BaseCheckinList):
)),
]
)
if self.event.has_subevents:
d['subevent'] = forms.ModelChoiceField(
self.event.subevents.all(),
label=pgettext_lazy('subevent', 'Date'),
required=False,
empty_label=pgettext_lazy('subevent', 'All dates')
)
return d
class CBFlowable(Flowable):
def __init__(self, checked=False):
self.checked = checked
super().__init__()
def draw(self):
self.canv.rect(1 * mm, -4.5 * mm, 4 * mm, 4 * mm)
if self.checked:
self.canv.line(1.5 * mm, -4.0 * mm, 4.5 * mm, -1.0 * mm)
self.canv.line(1.5 * mm, -1.0 * mm, 4.5 * mm, -4.0 * mm)
class TableTextRotate(Flowable):
def __init__(self, text):
Flowable.__init__(self)
self.text = text
def draw(self):
canvas = self.canv
canvas.rotate(90)
canvas.drawString(0, -1, self.text)
class PDFCheckinList(ReportlabExportMixin, BaseCheckinList):
name = "overview"
identifier = 'checkinlistpdf'
verbose_name = ugettext_lazy('Check-in list (PDF)')
@property
def export_form_fields(self):
f = super().export_form_fields
del f['secrets']
return f
@property
def pagesize(self):
from reportlab.lib import pagesizes
return pagesizes.landscape(pagesizes.A4)
def get_story(self, doc, form_data):
cl = self.event.checkin_lists.get(pk=form_data['list'])
questions = list(Question.objects.filter(event=self.event, id__in=form_data['questions']))
headlinestyle = self.get_style()
headlinestyle.fontSize = 15
headlinestyle.fontName = 'OpenSansBd'
colwidths = [3 * mm, 8 * mm, 8 * mm] + [
a * (doc.width - 8 * mm)
for a in [.1, .25, (.25 if questions else .60)] + (
[.35 / len(questions)] * len(questions) if questions else []
)
]
tstyledata = [
('VALIGN', (0, 0), (-1, 0), 'BOTTOM'),
('ALIGN', (2, 0), (2, 0), 'CENTER'),
('VALIGN', (0, 1), (-1, -1), 'TOP'),
('FONTNAME', (0, 0), (-1, 0), 'OpenSansBd'),
('ALIGN', (0, 0), (0, -1), 'CENTER'),
('TEXTCOLOR', (0, 0), (0, -1), '#990000'),
('FONTNAME', (0, 0), (0, -1), 'OpenSansBd'),
]
story = [
Paragraph(
'{} {}'.format(cl.name, (cl.subevent or self.event).get_date_from_display()),
headlinestyle
),
Spacer(1, 5 * mm)
]
tdata = [
[
'',
'',
# Translators: maximum 5 characters
TableTextRotate(pgettext('tablehead', 'paid')),
_('Order'),
_('Name'),
_('Product') + '\n' + _('Price'),
],
]
for q in questions:
tdata[0].append(str(q.question))
cqs = Checkin.objects.filter(
position_id=OuterRef('pk'),
list_id=cl.pk
).order_by().values('position_id').annotate(
m=Max('datetime')
).values('m')
qs = OrderPosition.objects.filter(
order__event=self.event,
).annotate(
last_checked_in=Subquery(cqs)
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address').prefetch_related(
'answers', 'answers__question'
)
if not cl.all_products:
qs = qs.filter(item__in=cl.limit_products.values_list('id', flat=True))
if cl.subevent:
qs = qs.filter(subevent=cl.subevent)
if form_data['sort'] == 'name':
qs = qs.order_by(Coalesce('attendee_name', 'addon_to__attendee_name', 'order__invoice_address__name'),
'order__code')
elif form_data['sort'] == 'code':
qs = qs.order_by('order__code')
if form_data['paid_only']:
qs = qs.filter(order__status=Order.STATUS_PAID)
else:
qs = qs.filter(order__status__in=(Order.STATUS_PAID, Order.STATUS_PENDING))
for op in qs:
try:
ian = op.order.invoice_address.name
iac = op.order.invoice_address.company
except:
ian = ""
iac = ""
name = op.attendee_name or (op.addon_to.attendee_name if op.addon_to else '') or ian
if iac:
name += "\n" + iac
row = [
'!!' if op.item.checkin_attention else '',
CBFlowable(bool(op.last_checked_in)),
'' if op.order.status != Order.STATUS_PAID else '',
op.order.code,
name,
str(op.item.name) + (" " + str(op.variation.value) if op.variation else "") + "\n" +
self.event.currency + " " + localize(op.price),
]
acache = {}
for a in op.answers.all():
acache[a.question_id] = str(a)
for q in questions:
row.append(acache.get(q.pk, ''))
if op.order.status != Order.STATUS_PAID:
tstyledata += [
('BACKGROUND', (2, len(tdata)), (2, len(tdata)), '#990000'),
('TEXTCOLOR', (2, len(tdata)), (2, len(tdata)), '#ffffff'),
('ALIGN', (2, len(tdata)), (2, len(tdata)), 'CENTER'),
]
tdata.append(row)
table = Table(tdata, colWidths=colwidths, repeatRows=1)
table.setStyle(TableStyle(tstyledata))
story.append(table)
return story
class CSVCheckinList(BaseCheckinList):
name = "overview"
identifier = 'checkinlistcsv'
verbose_name = ugettext_lazy('Check-in list (CSV)')
def render(self, form_data: dict):
output = io.StringIO()
writer = csv.writer(output, quoting=csv.QUOTE_NONNUMERIC, delimiter=",")
cl = self.event.checkin_lists.get(pk=form_data['list'])
questions = list(Question.objects.filter(event=self.event, id__in=form_data['questions']))
qs = OrderPosition.objects.filter(
order__event=self.event, item_id__in=form_data['items']
order__event=self.event,
).prefetch_related(
'answers', 'answers__question'
).select_related('order', 'item', 'variation', 'addon_to')
if not cl.all_products:
qs = qs.filter(item__in=cl.limit_products.values_list('id', flat=True))
if cl.subevent:
qs = qs.filter(subevent=cl.subevent)
if form_data['sort'] == 'name':
qs = qs.order_by(Coalesce('attendee_name', 'addon_to__attendee_name'))
elif form_data['sort'] == 'code':
@@ -95,8 +255,6 @@ class CSVCheckinList(BaseCheckinList):
headers = [
_('Order code'), _('Attendee name'), _('Product'), _('Price')
]
if form_data.get('subevent'):
qs = qs.filter(subevent=form_data.get('subevent'))
if form_data['paid_only']:
qs = qs.filter(order__status=Order.STATUS_PAID)
else:

View File

@@ -7,3 +7,9 @@ from pretix.base.signals import register_data_exporters
def register_csv(sender, **kwargs):
from .exporters import CSVCheckinList
return CSVCheckinList
@receiver(register_data_exporters, dispatch_uid="export_checkinlist_pdf")
def register_pdf(sender, **kwargs):
from .exporters import PDFCheckinList
return PDFCheckinList

View File

@@ -12,6 +12,7 @@ class PretixdroidApp(AppConfig):
name = _("pretixdroid API")
author = _("the pretix team")
version = version
visible = True
description = _("This plugin allows you to use the pretixdroid Android app for your event.")
def ready(self):

View File

@@ -6,7 +6,7 @@ from pretix.plugins.pretixdroid.models import AppConfiguration
class AppConfigurationForm(forms.ModelForm):
class Meta:
model = AppConfiguration
fields = ('all_items', 'items', 'subevent', 'show_info', 'allow_search')
fields = ('all_items', 'items', 'list', 'show_info', 'allow_search')
widgets = {
'items': forms.CheckboxSelectMultiple(attrs={
'data-inverse-dependency': '#id_all_items'
@@ -17,7 +17,4 @@ class AppConfigurationForm(forms.ModelForm):
self.event = kwargs.pop('event')
super().__init__(**kwargs)
self.fields['items'].queryset = self.event.items.all()
if self.event.has_subevents:
self.fields['subevent'].queryset = self.event.subevents.all()
else:
del self.fields['subevent']
self.fields['list'].queryset = self.event.checkin_lists.all()

View File

@@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-11-24 16:57
from __future__ import unicode_literals
import django.db.models.deletion
from django.db import migrations, models
def assign_checkin_lists(apps, schema_editor):
AppConfiguration = apps.get_model('pretixdroid', 'AppConfiguration')
for ac in AppConfiguration.objects.all():
cl = ac.event.checkin_lists.get_or_create(subevent=ac.subevent, all_products=True, defaults={
'name': ac.subevent.name if ac.subevent else 'Default'
})[0]
ac.list = cl
ac.save()
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0077_auto_20171124_1629'),
('pretixdroid', '0003_appconfiguration'),
]
operations = [
migrations.AddField(
model_name='appconfiguration',
name='list',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
to='pretixbase.CheckinList'),
),
migrations.AlterField(
model_name='appconfiguration',
name='all_items',
field=models.BooleanField(default=True, verbose_name='Can scan all products'),
),
migrations.AlterField(
model_name='appconfiguration',
name='allow_search',
field=models.BooleanField(default=True,
help_text='If disabled, the device can not search for attendees by name. pretixdroid 1.6 or newer only.',
verbose_name='Search allowed'),
),
migrations.AlterField(
model_name='appconfiguration',
name='items',
field=models.ManyToManyField(blank=True, to='pretixbase.Item', verbose_name='Can scan these products'),
),
migrations.AlterField(
model_name='appconfiguration',
name='show_info',
field=models.BooleanField(default=True,
help_text='If disabled, the device can not see how many tickets exist and how many are already scanned. pretixdroid 1.6 or newer only.',
verbose_name='Show information'),
),
migrations.RunPython(
assign_checkin_lists,
migrations.RunPython.noop
),
migrations.RemoveField(
model_name='appconfiguration',
name='subevent',
),
migrations.AlterField(
model_name='appconfiguration',
name='list',
field=models.ForeignKey(blank=False, null=False, on_delete=django.db.models.deletion.CASCADE,
to='pretixbase.CheckinList'),
),
]

View File

@@ -2,7 +2,7 @@ import string
from django.db import models
from django.utils.crypto import get_random_string
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django.utils.translation import ugettext_lazy as _
class AppConfiguration(models.Model):
@@ -10,14 +10,19 @@ class AppConfiguration(models.Model):
key = models.CharField(max_length=190, db_index=True)
all_items = models.BooleanField(default=True, verbose_name=_('Can scan all products'))
items = models.ManyToManyField('pretixbase.Item', blank=True, verbose_name=_('Can scan these products'))
subevent = models.ForeignKey('pretixbase.SubEvent', null=True, blank=True,
verbose_name=pgettext_lazy('subevent', 'Date'))
show_info = models.BooleanField(default=True, verbose_name=_('Show information'),
help_text=_('If disabled, the device can not see how many tickets exist and how '
'many are already scanned. pretixdroid 1.6 or newer only.'))
allow_search = models.BooleanField(default=True, verbose_name=_('Search allowed'),
help_text=_('If disabled, the device can not search for attendees by name. '
'pretixdroid 1.6 or newer only.'))
list = models.ForeignKey(
'pretixbase.CheckinList', on_delete=models.CASCADE, verbose_name=_('Check-in list')
)
@property
def subevent(self):
return self.list.subevent
def save(self, **kwargs):
if not self.key:

View File

@@ -7,6 +7,7 @@ from django.dispatch import receiver
from django.utils.formats import date_format
from django.utils.translation import ugettext_lazy as _
from pretix.base.models import CheckinList
from pretix.base.signals import logentry_display
from pretix.control.signals import nav_event
@@ -43,25 +44,40 @@ def pretixcontrol_logentry_display(sender, logentry, **kwargs):
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'):
if show_dt:
return _('Position #{posid} has been scanned at {datetime}.').format(
return _('Position #{posid} has been scanned at {datetime} for list "{list}".').format(
posid=data.get('positionid'),
datetime=dt_formatted
datetime=dt_formatted,
list=checkin_list
)
else:
return _('Position #{posid} has been scanned.').format(
posid=data.get('positionid')
return _('Position #{posid} has been scanned for list "{list}".').format(
posid=data.get('positionid'),
list=checkin_list
)
else:
if data.get('forced'):
return _(
'A scan for position #{posid} at {datetime} has been uploaded even though it has '
'A scan for position #{posid} at {datetime} for list "{list}" has been uploaded even though it has '
'been scanned already.'.format(
posid=data.get('positionid'),
datetime=dt_formatted
datetime=dt_formatted,
list=checkin_list
)
)
return _('Position #{posid} has been scanned and rejected because it has already been scanned before.'.format(
posid=data.get('positionid')
))
return _(
'Position #{posid} has been scanned and rejected because it has already been scanned before '
'on list "{list}".'.format(
posid=data.get('positionid'),
list=checkin_list
)
)

View File

@@ -19,13 +19,11 @@
<form action="?add" method="post" class="form-horizontal">
{% csrf_token %}
{% bootstrap_form_errors add_form %}
{% bootstrap_field add_form.list layout="horizontal" %}
{% bootstrap_field add_form.all_items layout="horizontal" %}
{% bootstrap_field add_form.items layout="horizontal" %}
{% bootstrap_field add_form.show_info layout="horizontal" %}
{% bootstrap_field add_form.allow_search layout="horizontal" %}
{% if add_form.subevent %}
{% bootstrap_field add_form.subevent layout="horizontal" %}
{% endif %}
<div class="form-group">
<div class="col-md-offset-3 col-md-9">
<button type="submit" class="btn btn-primary btn-save" name="add" value="1">
@@ -48,9 +46,7 @@
<thead>
<tr>
<th>{% trans "ID" %}</th>
{% if request.event.has_subevents %}
<th>{% trans "Date" context "subevent" %}</th>
{% endif %}
<th>{% trans "Check-in list" %}</th>
<th>{% trans "Items" %}</th>
<th>{% trans "Show info" %}</th>
<th>{% trans "Allow search" %}</th>
@@ -61,9 +57,9 @@
{% for ac in configs %}
<tr>
<td>{{ ac.key|slice:"0:8" }}…</td>
{% if request.event.has_subevents %}
<td>{% if ac.subevent %}{{ ac.subevent }}{% else %}{% trans "All" %}{% endif %}</td>
{% endif %}
<td>
<a href="{% url "control:event.orders.checkinlists.show" organizer=request.event.organizer.slug event=request.event.slug list=ac.list.id %}">{{ ac.list }}</a>
</td>
<td>
{% if ac.all_items %}
{% trans "All" %}

View File

@@ -4,7 +4,7 @@ import logging
import dateutil.parser
from django.contrib import messages
from django.db import transaction
from django.db.models import Count, Q
from django.db.models import Count, Max, OuterRef, Q, Subquery
from django.http import (
HttpResponseForbidden, HttpResponseNotFound, JsonResponse,
)
@@ -110,7 +110,7 @@ class ConfigView(EventPermissionRequiredMixin, TemplateView):
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
ctx['add_form'] = self.add_form
ctx['configs'] = self.request.event.appconfiguration_set.prefetch_related('items')
ctx['configs'] = self.request.event.appconfiguration_set.select_related('list').prefetch_related('items')
return ctx
@@ -133,8 +133,10 @@ class ApiView(View):
self.subevent = None
if self.event.has_subevents:
if self.config.subevent:
self.subevent = self.config.subevent
if self.config.list.subevent:
self.subevent = self.config.list.subevent
if 'subevent' in kwargs and kwargs['subevent'] != str(self.subevent.pk):
return HttpResponseForbidden('Invalid subevent selected.')
elif 'subevent' in kwargs:
self.subevent = get_object_or_404(SubEvent, event=self.event, pk=kwargs['subevent'])
else:
@@ -166,11 +168,14 @@ class ApiRedeemView(ApiView):
op = OrderPosition.objects.select_related('item', 'variation', 'order', 'addon_to').get(
order__event=self.event, secret=secret, subevent=self.subevent
)
if not self.config.list.all_products and op.item_id not in [i.pk for i in self.config.list.limit_products.all()]:
response['status'] = 'error'
response['reason'] = 'product'
if not self.config.all_items and op.item_id not in [i.pk for i in self.config.items.all()]:
response['status'] = 'error'
response['reason'] = 'product'
elif op.order.status == Order.STATUS_PAID or force:
ci, created = Checkin.objects.get_or_create(position=op, defaults={
ci, created = Checkin.objects.get_or_create(position=op, list=self.config.list, defaults={
'datetime': dt,
'nonce': nonce,
})
@@ -188,6 +193,7 @@ class ApiRedeemView(ApiView):
'first': True,
'forced': op.order.status != Order.STATUS_PAID,
'datetime': dt,
'list': self.config.list.pk
})
else:
if force:
@@ -201,6 +207,7 @@ class ApiRedeemView(ApiView):
'first': False,
'forced': force,
'datetime': dt,
'list': self.config.list.pk
})
response['data'] = serialize_op(op, redeemed=op.order.status == Order.STATUS_PAID or force)
@@ -243,22 +250,38 @@ class ApiSearchView(ApiView):
}
if len(query) >= 4:
qs = OrderPosition.objects.select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address')
cqs = Checkin.objects.filter(
position_id=OuterRef('pk'),
list_id=self.config.list.pk
).order_by().values('position_id').annotate(
m=Max('datetime')
).values('m')
qs = OrderPosition.objects.filter(
order__event=self.event,
order__status=Order.STATUS_PAID,
subevent=self.config.list.subevent
).annotate(
last_checked_in=Subquery(cqs)
).select_related('item', 'variation', 'order', 'order__invoice_address', 'addon_to')
if not self.config.list.all_products:
qs = qs.filter(item__in=self.config.list.limit_products.values_list('id', flat=True))
if not self.config.all_items:
qs = qs.filter(item__in=self.config.items.all())
if not self.config.allow_search:
ops = qs.filter(
Q(order__event=self.event) & Q(secret__istartswith=query) & Q(subevent=self.subevent)
).annotate(checkin_cnt=Count('checkins'))[:25]
Q(secret__istartswith=query)
)[:25]
else:
ops = qs.filter(
Q(order__event=self.event)
& Q(
Q(secret__istartswith=query) | Q(attendee_name__icontains=query) | Q(order__code__istartswith=query)
| Q(order__invoice_address__name__icontains=query)
)
& Q(subevent=self.subevent)
).annotate(checkin_cnt=Count('checkins'))[:25]
Q(secret__istartswith=query) | Q(attendee_name__icontains=query) | Q(order__code__istartswith=query)
| Q(order__invoice_address__name__icontains=query)
)[:25]
response['results'] = [serialize_op(op, bool(op.checkin_cnt)) for op in ops]
response['results'] = [serialize_op(op, bool(op.last_checked_in)) for op in ops]
else:
response['results'] = []
@@ -271,25 +294,51 @@ class ApiDownloadView(ApiView):
'version': API_VERSION
}
ops = OrderPosition.objects.select_related('item', 'variation', 'order', 'addon_to').filter(
Q(order__event=self.event) & Q(subevent=self.subevent)
)
cqs = Checkin.objects.filter(
position_id=OuterRef('pk'),
list_id=self.config.list.pk
).order_by().values('position_id').annotate(
m=Max('datetime')
).values('m')
qs = OrderPosition.objects.filter(
order__event=self.event,
order__status=Order.STATUS_PAID,
subevent=self.config.list.subevent
).annotate(
last_checked_in=Subquery(cqs)
).select_related('item', 'variation', 'order', 'addon_to')
if not self.config.list.all_products:
qs = qs.filter(item__in=self.config.list.limit_products.values_list('id', flat=True))
if not self.config.all_items:
ops = ops.filter(item__in=self.config.items.all())
ops = ops.annotate(checkin_cnt=Count('checkins'))
response['results'] = [serialize_op(op, bool(op.checkin_cnt)) for op in ops]
qs = qs.filter(item__in=self.config.items.all())
response['results'] = [serialize_op(op, bool(op.last_checked_in)) for op in qs]
return JsonResponse(response)
class ApiStatusView(ApiView):
def get(self, request, **kwargs):
cqs = Checkin.objects.filter(
position__order__event=self.event, position__subevent=self.subevent,
position__order__status=Order.STATUS_PAID,
list=self.config.list
)
pqs = OrderPosition.objects.filter(
order__event=self.event, order__status=Order.STATUS_PAID, subevent=self.subevent,
)
if not self.config.list.all_products:
pqs = pqs.filter(item__in=self.config.list.limit_products.values_list('id', flat=True))
ev = self.subevent or self.event
response = {
'version': API_VERSION,
'event': {
'name': str(ev.name),
'list': self.config.list.name,
'slug': self.event.slug,
'organizer': {
'name': str(self.event.organizer),
@@ -301,45 +350,25 @@ class ApiStatusView(ApiView):
'timezone': self.event.settings.timezone,
'url': event_absolute_uri(self.event, 'presale:event.index')
},
'checkins': Checkin.objects.filter(
position__order__event=self.event, position__subevent=self.subevent
).count(),
'total': OrderPosition.objects.filter(
order__event=self.event, order__status=Order.STATUS_PAID, subevent=self.subevent
).count()
'checkins': cqs.count(),
'total': pqs.count()
}
op_by_item = {
p['item']: p['cnt']
for p in OrderPosition.objects.filter(
order__event=self.event,
order__status=Order.STATUS_PAID,
subevent=self.subevent
).order_by().values('item').annotate(cnt=Count('id'))
for p in pqs.order_by().values('item').annotate(cnt=Count('id'))
}
op_by_variation = {
p['variation']: p['cnt']
for p in OrderPosition.objects.filter(
order__event=self.event,
order__status=Order.STATUS_PAID,
subevent=self.subevent
).order_by().values('variation').annotate(cnt=Count('id'))
for p in pqs.order_by().values('variation').annotate(cnt=Count('id'))
}
c_by_item = {
p['position__item']: p['cnt']
for p in Checkin.objects.filter(
position__order__event=self.event,
position__order__status=Order.STATUS_PAID,
position__subevent=self.subevent
).order_by().values('position__item').annotate(cnt=Count('id'))
for p in cqs.order_by().values('position__item').annotate(cnt=Count('id'))
}
c_by_variation = {
p['position__variation']: p['cnt']
for p in Checkin.objects.filter(
position__order__event=self.event,
position__order__status=Order.STATUS_PAID,
position__subevent=self.subevent
).order_by().values('position__variation').annotate(cnt=Count('id'))
for p in cqs.order_by().values('position__variation').annotate(cnt=Count('id'))
}
response['items'] = []

View File

@@ -18,17 +18,7 @@ from pretix.base.models.orders import OrderFee
from pretix.base.services.stats import order_overview
class Report(BaseExporter):
name = "report"
def verbose_name(self) -> str:
raise NotImplementedError()
def identifier(self) -> str:
raise NotImplementedError()
def __init__(self, event):
super().__init__(event)
class ReportlabExportMixin:
@property
def pagesize(self):
@@ -38,7 +28,7 @@ class Report(BaseExporter):
def render(self, form_data):
self.form_data = form_data
return 'report-%s.pdf' % self.event.slug, 'application/pdf', self.create()
return 'report-%s.pdf' % self.event.slug, 'application/pdf', self.create(form_data)
def get_filename(self):
tz = pytz.timezone(self.event.settings.timezone)
@@ -53,7 +43,7 @@ class Report(BaseExporter):
pdfmetrics.registerFont(TTFont('OpenSansIt', finders.find('fonts/OpenSans-Italic.ttf')))
pdfmetrics.registerFont(TTFont('OpenSansBd', finders.find('fonts/OpenSans-Bold.ttf')))
def create(self):
def create(self, form_data):
from reportlab.platypus import BaseDocTemplate, PageTemplate
from reportlab.lib.units import mm
@@ -67,7 +57,7 @@ class Report(BaseExporter):
doc.addPageTemplates([
PageTemplate(id='All', frames=self.get_frames(doc), onPage=self.on_page, pagesize=self.pagesize)
])
doc.build(self.get_story(doc))
doc.build(self.get_story(doc, form_data))
f.seek(0)
return f.read()
@@ -84,7 +74,7 @@ class Report(BaseExporter):
id='normal')
return [self.frame]
def get_story(self, doc):
def get_story(self, doc, form_data):
return []
def get_style(self):
@@ -122,6 +112,19 @@ class Report(BaseExporter):
self.pagesize[0] - 15 * mm, self.pagesize[1] - 17 * mm)
class Report(ReportlabExportMixin, BaseExporter):
name = "report"
def verbose_name(self) -> str:
raise NotImplementedError()
def identifier(self) -> str:
raise NotImplementedError()
def __init__(self, event):
super().__init__(event)
class OverviewReport(Report):
name = "overview"
identifier = 'pdfreport'
@@ -133,7 +136,7 @@ class OverviewReport(Report):
return pagesizes.landscape(pagesizes.A4)
def get_story(self, doc):
def get_story(self, doc, form_data):
from reportlab.platypus import Paragraph, Spacer, TableStyle, Table
from reportlab.lib.units import mm
@@ -290,7 +293,7 @@ class OrderTaxListReport(Report):
return pagesizes.landscape(pagesizes.A4)
def get_story(self, doc):
def get_story(self, doc, form_data):
from reportlab.platypus import Paragraph, Spacer, TableStyle, Table
from reportlab.lib.units import mm

View File

@@ -261,7 +261,8 @@ REST_FRAMEWORK = {
CORE_MODULES = {
"pretix.base",
"pretix.presale",
"pretix.control"
"pretix.control",
"pretix.plugins.checkinlists",
}
MIDDLEWARE = [

View File

@@ -44,6 +44,199 @@ $(document).ajaxError(function (event, jqXHR, settings, thrownError) {
}
});
var form_handlers = function (el) {
el.find(".datetimepicker").each(function() {
$(this).datetimepicker({
format: $("body").attr("data-datetimeformat"),
locale: $("body").attr("data-datetimelocale"),
useCurrent: false,
showClear: !$(this).prop("required"),
icons: {
time: 'fa fa-clock-o',
date: 'fa fa-calendar',
up: 'fa fa-chevron-up',
down: 'fa fa-chevron-down',
previous: 'fa fa-chevron-left',
next: 'fa fa-chevron-right',
today: 'fa fa-screenshot',
clear: 'fa fa-trash',
close: 'fa fa-remove'
}
});
if (!$(this).val()) {
$(this).data("DateTimePicker").viewDate(moment().hour(0).minute(0).second(0));
}
});
el.find(".datepickerfield").each(function() {
var opts = {
format: $("body").attr("data-dateformat"),
locale: $("body").attr("data-datetimelocale"),
useCurrent: false,
showClear: !$(this).prop("required"),
icons: {
time: 'fa fa-clock-o',
date: 'fa fa-calendar',
up: 'fa fa-chevron-up',
down: 'fa fa-chevron-down',
previous: 'fa fa-chevron-left',
next: 'fa fa-chevron-right',
today: 'fa fa-screenshot',
clear: 'fa fa-trash',
close: 'fa fa-remove'
},
};
if ($(this).is('[data-is-payment-date]'))
opts["daysOfWeekDisabled"] = JSON.parse($("body").attr("data-payment-weekdays-disabled"));
$(this).datetimepicker(opts);
if ($(this).parent().is('.splitdatetimerow')) {
$(this).on("dp.change", function (ev) {
var $timepicker = $(this).closest(".splitdatetimerow").find(".timepickerfield");
var date = $(this).data('DateTimePicker').date();
if (date === null) {
return;
}
if ($timepicker.val() === "") {
date.set({'hour': 0, 'minute': 0, 'second': 0});
$timepicker.data('DateTimePicker').date(date);
}
});
}
});
el.find(".timepickerfield").each(function() {
var opts = {
format: $("body").attr("data-timeformat"),
locale: $("body").attr("data-datetimelocale"),
useCurrent: false,
showClear: !$(this).prop("required"),
icons: {
time: 'fa fa-clock-o',
date: 'fa fa-calendar',
up: 'fa fa-chevron-up',
down: 'fa fa-chevron-down',
previous: 'fa fa-chevron-left',
next: 'fa fa-chevron-right',
today: 'fa fa-screenshot',
clear: 'fa fa-trash',
close: 'fa fa-remove'
}
};
if ($(this).is('[data-is-payment-date]'))
opts["daysOfWeekDisabled"] = JSON.parse($("body").attr("data-payment-weekdays-disabled"));
$(this).datetimepicker(opts);
});
el.find(".datetimepicker[data-date-after], .datepickerfield[data-date-after]").each(function() {
var later_field = $(this),
earlier_field = $($(this).attr("data-date-after")),
update = function () {
var earlier = earlier_field.data('DateTimePicker').date(),
later = later_field.data('DateTimePicker').date();
if (earlier === null) {
earlier = false;
} else if (later !== null && later.isBefore(earlier) && !later.isSame(earlier)) {
later_field.data('DateTimePicker').date(earlier.add(1, 'h'));
}
later_field.data('DateTimePicker').minDate(earlier);
};
update();
earlier_field.on("dp.change", update);
});
el.find(".datetimepicker[data-date-default]").each(function() {
var fill_field = $(this),
default_field = $($(this).attr("data-date-default")),
show = function () {
var fill_date = fill_field.data('DateTimePicker').date(),
default_date = default_field.data('DateTimePicker').date();
if (fill_date === null) {
fill_field.data("DateTimePicker").defaultDate(default_date);
}
};
fill_field.on("dp.show", show);
});
el.find(".colorpickerfield").colorpicker({
format: 'hex',
align: 'left',
customClass: 'colorpicker-2x',
sliders: {
saturation: {
maxLeft: 200,
maxTop: 200
},
hue: {
maxTop: 200
},
alpha: {
maxTop: 200
}
}
});
el.find("input[data-checkbox-dependency]").each(function () {
var dependent = $(this),
dependency = $($(this).attr("data-checkbox-dependency")),
update = function () {
var enabled = dependency.prop('checked');
dependent.prop('disabled', !enabled).parents('.form-group').toggleClass('disabled', !enabled);
if (!enabled) {
dependent.prop('checked', false);
}
};
update();
dependency.on("change", update);
});
el.find("input[data-inverse-dependency]").each(function () {
var dependency = $(this).attr("data-inverse-dependency");
if (dependency.substr(0, 1) === '<') {
dependency = $(this).closest("form, .form-horizontal").find(dependency.substr(1));
} else {
dependency = $(dependency);
}
var dependent = $(this),
update = function () {
var enabled = !dependency.prop('checked');
dependent.prop('disabled', !enabled).parents('.form-group').toggleClass('disabled', !enabled);
};
update();
dependency.on("change", update);
});
el.find("input[data-display-dependency]").each(function () {
var dependent = $(this),
dependency = $($(this).attr("data-display-dependency")),
update = function () {
var enabled = (dependency.attr("type") === 'checkbox') ? dependency.prop('checked') : !!dependency.val();
dependent.prop('disabled', !enabled).parents('.form-group').toggleClass('disabled', !enabled);
};
update();
dependency.on("change", update);
dependency.on("dp.change", update);
});
el.find(".scrolling-multiple-choice").each(function () {
var $small = $("<small>");
var $a_all = $("<a>").addClass("choice-options-all").attr("href", "#").text(gettext("All"));
var $a_none = $("<a>").addClass("choice-options-none").attr("href", "#").text(gettext("None"));
$(this).prepend($small.append($a_all).append(" / ").append($a_none));
$(this).find(".choice-options-none").click(function (e) {
$(this).closest(".scrolling-multiple-choice").find("input[type=checkbox]").prop("checked", false);
e.preventDefault();
return false;
});
$(this).find(".choice-options-all").click(function (e) {
$(this).closest(".scrolling-multiple-choice").find("input[type=checkbox]").prop("checked", true);
e.preventDefault();
return false;
});
})
}
$(function () {
"use strict";
@@ -53,6 +246,9 @@ $(function () {
reorderMode: 'animate'
}
);
$("[data-formset]").on("formAdded", "div", function (event) {
form_handlers($(event.target));
});
$(document).on("click", ".variations .variations-select-all", function (e) {
$(this).parent().parent().find("input[type=checkbox]").prop("checked", true).change();
e.stopPropagation();
@@ -137,175 +333,7 @@ $(function () {
}
});
$("#ajaxerr").on("click", ".ajaxerr-close", ajaxErrDialog.hide);
moment.locale($("body").attr("data-datetimelocale"));
$(".datetimepicker").each(function() {
$(this).datetimepicker({
format: $("body").attr("data-datetimeformat"),
locale: $("body").attr("data-datetimelocale"),
useCurrent: false,
showClear: !$(this).prop("required"),
icons: {
time: 'fa fa-clock-o',
date: 'fa fa-calendar',
up: 'fa fa-chevron-up',
down: 'fa fa-chevron-down',
previous: 'fa fa-chevron-left',
next: 'fa fa-chevron-right',
today: 'fa fa-screenshot',
clear: 'fa fa-trash',
close: 'fa fa-remove'
}
});
if (!$(this).val()) {
$(this).data("DateTimePicker").viewDate(moment().hour(0).minute(0).second(0));
}
});
$(".datepickerfield").each(function() {
var opts = {
format: $("body").attr("data-dateformat"),
locale: $("body").attr("data-datetimelocale"),
useCurrent: false,
showClear: !$(this).prop("required"),
icons: {
time: 'fa fa-clock-o',
date: 'fa fa-calendar',
up: 'fa fa-chevron-up',
down: 'fa fa-chevron-down',
previous: 'fa fa-chevron-left',
next: 'fa fa-chevron-right',
today: 'fa fa-screenshot',
clear: 'fa fa-trash',
close: 'fa fa-remove'
},
};
if ($(this).is('[data-is-payment-date]'))
opts["daysOfWeekDisabled"] = JSON.parse($("body").attr("data-payment-weekdays-disabled"));
$(this).datetimepicker(opts);
if ($(this).parent().is('.splitdatetimerow')) {
$(this).on("dp.change", function (ev) {
var $timepicker = $(this).closest(".splitdatetimerow").find(".timepickerfield");
var date = $(this).data('DateTimePicker').date();
if (date === null) {
return;
}
if ($timepicker.val() === "") {
date.set({'hour': 0, 'minute': 0, 'second': 0});
$timepicker.data('DateTimePicker').date(date);
}
});
}
});
$(".timepickerfield").each(function() {
var opts = {
format: $("body").attr("data-timeformat"),
locale: $("body").attr("data-datetimelocale"),
useCurrent: false,
showClear: !$(this).prop("required"),
icons: {
time: 'fa fa-clock-o',
date: 'fa fa-calendar',
up: 'fa fa-chevron-up',
down: 'fa fa-chevron-down',
previous: 'fa fa-chevron-left',
next: 'fa fa-chevron-right',
today: 'fa fa-screenshot',
clear: 'fa fa-trash',
close: 'fa fa-remove'
}
};
if ($(this).is('[data-is-payment-date]'))
opts["daysOfWeekDisabled"] = JSON.parse($("body").attr("data-payment-weekdays-disabled"));
$(this).datetimepicker(opts);
});
$(".datetimepicker[data-date-after], .datepickerfield[data-date-after]").each(function() {
var later_field = $(this),
earlier_field = $($(this).attr("data-date-after")),
update = function () {
var earlier = earlier_field.data('DateTimePicker').date(),
later = later_field.data('DateTimePicker').date();
if (earlier === null) {
earlier = false;
} else if (later !== null && later.isBefore(earlier) && !later.isSame(earlier)) {
later_field.data('DateTimePicker').date(earlier.add(1, 'h'));
}
later_field.data('DateTimePicker').minDate(earlier);
};
update();
earlier_field.on("dp.change", update);
});
$(".datetimepicker[data-date-default]").each(function() {
var fill_field = $(this),
default_field = $($(this).attr("data-date-default")),
show = function () {
var fill_date = fill_field.data('DateTimePicker').date(),
default_date = default_field.data('DateTimePicker').date();
if (fill_date === null) {
fill_field.data("DateTimePicker").defaultDate(default_date);
}
};
fill_field.on("dp.show", show);
});
$(".colorpickerfield").colorpicker({
format: 'hex',
align: 'left',
customClass: 'colorpicker-2x',
sliders: {
saturation: {
maxLeft: 200,
maxTop: 200
},
hue: {
maxTop: 200
},
alpha: {
maxTop: 200
}
}
});
$("input[data-checkbox-dependency]").each(function () {
var dependent = $(this),
dependency = $($(this).attr("data-checkbox-dependency")),
update = function () {
var enabled = dependency.prop('checked');
dependent.prop('disabled', !enabled).parents('.form-group').toggleClass('disabled', !enabled);
if (!enabled) {
dependent.prop('checked', false);
}
};
update();
dependency.on("change", update);
});
$("input[data-inverse-dependency]").each(function () {
var dependent = $(this),
dependency = $($(this).attr("data-inverse-dependency")),
update = function () {
var enabled = !dependency.prop('checked');
dependent.prop('disabled', !enabled).parents('.form-group').toggleClass('disabled', !enabled);
};
update();
dependency.on("change", update);
});
$("input[data-display-dependency]").each(function () {
var dependent = $(this),
dependency = $($(this).attr("data-display-dependency")),
update = function () {
var enabled = (dependency.attr("type") === 'checkbox') ? dependency.prop('checked') : !!dependency.val();
dependent.prop('disabled', !enabled).parents('.form-group').toggleClass('disabled', !enabled);
};
update();
dependency.on("change", update);
dependency.on("dp.change", update);
});
form_handlers($("body"));
$(".qrcode-canvas").each(function() {
$(this).qrcode(
@@ -324,21 +352,6 @@ $(function () {
return true;
})
$(".scrolling-multiple-choice").each(function () {
var $small = $("<small>");
var $a_all = $("<a>").addClass("choice-options-all").attr("href", "#").text(gettext("All"));
var $a_none = $("<a>").addClass("choice-options-none").attr("href", "#").text(gettext("None"));
$(this).prepend($small.append($a_all).append(" / ").append($a_none));
$(this).find(".choice-options-none").click(function (e) {
$(this).closest(".scrolling-multiple-choice").find("input[type=checkbox]").prop("checked", false);
e.preventDefault();
return false;
});
$(this).find(".choice-options-all").click(function (e) {
$(this).closest(".scrolling-multiple-choice").find("input[type=checkbox]").prop("checked", true);
e.preventDefault();
return false;
});
})
$("#ajaxerr").on("click", ".ajaxerr-close", ajaxErrDialog.hide);
moment.locale($("body").attr("data-datetimelocale"));
});

View File

@@ -0,0 +1,16 @@
/*globals $, Morris, gettext*/
$(function () {
if (!$("div[data-formset-prefix=checkinlist_set]").length) {
return;
}
var $namef = $("input[id^=id_name]").first();
var lastValue = $namef.val();
$namef.change(function () {
var field = $("div[data-formset-prefix=checkinlist_set] input[id$=name]").first();
if (field.val() === lastValue) {
lastValue = $(this).val();
field.val(lastValue);
}
});
});

View File

@@ -251,7 +251,7 @@ label .optional {
}
}
div.scrolling-multiple-choice {
div.scrolling-multiple-choice, div.scrolling-choice {
height: 150px;
border: 1px solid $input-border;
border-radius: $input-border-radius;

View File

@@ -8,7 +8,7 @@ django-formset-js-improved==0.5.0.1
django-compressor==2.1.1
django-hierarkey==1.0.*,>=1.0.3
django-filter==1.0.*
reportlab==3.2.*
reportlab==3.4.*
PyPDF2==1.26.*
easy-thumbnails==2.4.*
django-libsass

View File

@@ -73,7 +73,7 @@ setup(
'django-compressor==2.1',
'django-hierarkey==1.0.*,>=1.0.2',
'django-filter==1.0.*',
'reportlab==3.2.*',
'reportlab==3.4.*',
'easy-thumbnails==2.4.*',
'PyPDF2==1.26.*',
'django-libsass',

View File

@@ -0,0 +1,168 @@
import pytest
from pretix.base.models import CheckinList
@pytest.fixture
def item(event):
return event.items.create(name="Budget Ticket", default_price=23)
@pytest.fixture
def item_on_wrong_event(event2):
return event2.items.create(name="Budget Ticket", default_price=23)
@pytest.fixture
def other_item(event):
return event.items.create(name="Budget Ticket", default_price=23)
TEST_LIST_RES = {
"name": "Default",
"all_products": False,
"limit_products": [],
"position_count": 0,
"checkin_count": 0,
"subevent": None
}
@pytest.fixture
def clist(event, item):
c = event.checkin_lists.create(name="Default", all_products=False)
c.limit_products.add(item)
return c
@pytest.mark.django_db
def test_list_list(token_client, organizer, event, clist, item, subevent):
res = dict(TEST_LIST_RES)
res["id"] = clist.pk
res["limit_products"] = [item.pk]
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/'.format(organizer.slug, event.slug))
assert resp.status_code == 200
assert [res] == resp.data['results']
clist.subevent = subevent
clist.save()
res["subevent"] = subevent.pk
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/checkinlists/?subevent={}'.format(organizer.slug, event.slug, subevent.pk))
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/checkinlists/?subevent={}'.format(organizer.slug, event.slug, subevent.pk + 1))
assert [] == resp.data['results']
@pytest.mark.django_db
def test_list_detail(token_client, organizer, event, clist, item):
res = dict(TEST_LIST_RES)
res["id"] = clist.pk
res["limit_products"] = [item.pk]
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/'.format(organizer.slug, event.slug,
clist.pk))
assert resp.status_code == 200
assert res == resp.data
@pytest.mark.django_db
def test_list_create(token_client, organizer, event, item, item_on_wrong_event):
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/checkinlists/'.format(organizer.slug, event.slug),
{
"name": "VIP",
"limit_products": [item.pk],
"all_products": False,
"subevent": None
},
format='json'
)
assert resp.status_code == 201
cl = CheckinList.objects.get(pk=resp.data['id'])
assert cl.name == "VIP"
assert cl.limit_products.count() == 1
assert not cl.all_products
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/checkinlists/'.format(organizer.slug, event.slug),
{
"name": "VIP",
"limit_products": [item_on_wrong_event.pk],
"all_products": True,
"subevent": None
},
format='json'
)
assert resp.status_code == 400
assert resp.content.decode() == '{"non_field_errors":["One or more items do not belong to this event."]}'
@pytest.mark.django_db
def test_list_create_with_subevent(token_client, organizer, event, event3, item, subevent, subevent2):
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/checkinlists/'.format(organizer.slug, event.slug),
{
"name": "VIP",
"limit_products": [item.pk],
"all_products": True,
"subevent": subevent.pk
},
format='json'
)
assert resp.status_code == 201
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/checkinlists/'.format(organizer.slug, event.slug),
{
"name": "VIP",
"limit_products": [item.pk],
"all_products": True,
"subevent": None
},
format='json'
)
assert resp.status_code == 400
assert resp.content.decode() == '{"non_field_errors":["Subevent cannot be null for event series."]}'
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/checkinlists/'.format(organizer.slug, event.slug),
{
"name": "VIP",
"limit_products": [],
"all_products": True,
"subevent": subevent2.pk
},
format='json'
)
assert resp.status_code == 400
assert resp.content.decode() == '{"non_field_errors":["The subevent does not belong to this event."]}'
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/checkinlists/'.format(organizer.slug, event3.slug),
{
"name": "VIP",
"limit_products": [],
"all_products": True,
"subevent": subevent2.pk
},
format='json'
)
assert resp.status_code == 400
assert resp.content.decode() == '{"non_field_errors":["The subevent does not belong to this event."]}'
@pytest.mark.django_db
def test_list_update(token_client, organizer, event, clist):
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/checkinlists/{}/'.format(organizer.slug, event.slug, clist.pk),
{
"name": "VIP",
},
format='json'
)
assert resp.status_code == 200
cl = CheckinList.objects.get(pk=resp.data['id'])
assert cl.name == "VIP"

View File

@@ -247,7 +247,8 @@ def test_orderposition_list(token_client, organizer, event, order, item, subeven
'/api/v1/organizers/{}/events/{}/orderpositions/?has_checkin=true'.format(organizer.slug, event.slug))
assert [] == resp.data['results']
op.checkins.create(datetime=datetime.datetime(2017, 12, 26, 10, 0, 0, tzinfo=UTC))
cl = event.checkin_lists.create(name="Default")
op.checkins.create(datetime=datetime.datetime(2017, 12, 26, 10, 0, 0, tzinfo=UTC), list=cl)
res['checkins'] = [{'datetime': '2017-12-26T10:00:00Z'}]
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?has_checkin=true'.format(organizer.slug, event.slug))

View File

@@ -17,6 +17,7 @@ event_urls = [
'subevents/',
'taxrules/',
'waitinglistentries/',
'checkinlists/',
]
event_permission_urls = [
@@ -46,6 +47,11 @@ event_permission_urls = [
('post', 'can_change_orders', 'orders/ABC12/mark_expired/', 404),
('post', 'can_change_orders', 'orders/ABC12/mark_canceled/', 404),
('post', 'can_change_orders', 'orders/ABC12/extend/', 400),
('get', 'can_view_orders', 'checkinlists/', 200),
('post', 'can_change_event_settings', 'checkinlists/', 400),
('put', 'can_change_event_settings', 'checkinlists/1/', 404),
('patch', 'can_change_event_settings', 'checkinlists/1/', 404),
('delete', 'can_change_event_settings', 'checkinlists/1/', 404),
]

View File

@@ -983,6 +983,8 @@ class EventTest(TestCase):
que1.items.add(i1)
event1.settings.foo_setting = 23
event1.settings.tax_rate_default = tr7
cl1 = event1.checkin_lists.create(name="All", all_products=False)
cl1.limit_products.add(i1)
event2 = Event.objects.create(
organizer=self.organizer, name='Download', slug='ab1234',
@@ -990,7 +992,7 @@ class EventTest(TestCase):
)
event2.copy_data_from(event1)
for a in (tr7, c1, c2, i1, q1, que1):
for a in (tr7, c1, c2, i1, q1, que1, cl1):
a.refresh_from_db()
assert a.event == event1
@@ -1012,6 +1014,8 @@ class EventTest(TestCase):
assert que1new.items.get(pk=i1new.pk)
assert event2.settings.foo_setting == '23'
assert event2.settings.tax_rate_default == trnew
assert event2.checkin_lists.count() == 1
assert [i.pk for i in event2.checkin_lists.first().limit_products.all()] == [i1new.pk]
class SubEventTest(TestCase):

View File

@@ -1,4 +1,4 @@
from datetime import timedelta
from datetime import datetime, timedelta, timezone
from decimal import Decimal
import pytest
@@ -10,6 +10,8 @@ from pretix.base.models import (
)
from pretix.control.views.dashboards import checkin_widget
from ..base import SoupTest, extract_form_fields
@pytest.fixture
def dashboard_env():
@@ -27,6 +29,8 @@ def dashboard_env():
t.members.add(user)
t.limit_events.add(event)
cl = event.checkin_lists.create(name="Default", all_products=True)
event.settings.set('attendee_names_asked', True)
event.settings.set('locales', ['en', 'de'])
@@ -50,7 +54,7 @@ def dashboard_env():
price=Decimal("10")
)
return event, user, o, order_paid, item_ticket, item_mascot
return event, user, o, order_paid, item_ticket, item_mascot, cl
@pytest.mark.django_db
@@ -84,32 +88,11 @@ def test_dashboard_with_checkin(dashboard_env):
order=dashboard_env[3],
item=dashboard_env[4]
)
Checkin.objects.create(position=op)
Checkin.objects.create(position=op, list=dashboard_env[6])
c = checkin_widget(dashboard_env[0])
assert '1/2' in c[0]['content']
@pytest.mark.django_db
def test_dashboard_exclude_non_admission_item(dashboard_env):
dashboard_env[0].settings.ticket_download_nonadm = False
dashboard_env[0].save()
c = checkin_widget(dashboard_env[0])
assert '0/1' in c[0]['content']
@pytest.mark.django_db
def test_dashboard_exclude_non_admission_item_with_checkin(dashboard_env):
dashboard_env[0].settings.ticket_download_nonadm = False
dashboard_env[0].save()
op = OrderPosition.objects.get(
order=dashboard_env[3],
item=dashboard_env[4]
)
Checkin.objects.create(position=op)
c = checkin_widget(dashboard_env[0])
assert '1/1' in c[0]['content']
@pytest.fixture
def checkin_list_env():
# permission
@@ -128,9 +111,11 @@ def checkin_list_env():
event.settings.set('locales', ['en', 'de'])
team.limit_events.add(event)
cl = event.checkin_lists.create(name="Default", all_products=True)
# item
item_ticket = Item.objects.create(event=event, name="Ticket", default_price=23, admission=True)
item_mascot = Item.objects.create(event=event, name="Mascot", default_price=10, admission=False)
item_ticket = Item.objects.create(event=event, name="Ticket", default_price=23, admission=True, position=0)
item_mascot = Item.objects.create(event=event, name="Mascot", default_price=10, admission=False, position=1)
# order
order_pending = Order.objects.create(
@@ -196,32 +181,32 @@ def checkin_list_env():
)
# checkin
Checkin.objects.create(position=op_a1_ticket, datetime=now() + timedelta(minutes=1))
Checkin.objects.create(position=op_a3_ticket)
Checkin.objects.create(position=op_a1_ticket, datetime=now() + timedelta(minutes=1), list=cl)
Checkin.objects.create(position=op_a3_ticket, list=cl)
return event, user, orga, [item_ticket, item_mascot], [order_pending, order_a1, order_a2, order_a3], \
[op_pending_ticket, op_a1_ticket, op_a1_mascot, op_a2_ticket, op_a3_ticket]
[op_pending_ticket, op_a1_ticket, op_a1_mascot, op_a2_ticket, op_a3_ticket], cl
@pytest.mark.django_db
@pytest.mark.parametrize("order_key, expected", [
('', ['A1Ticket', 'A1Mascot', 'A2Ticket', 'A3Ticket']),
('-code', ['A3Ticket', 'A2Ticket', 'A1Ticket', 'A1Mascot']),
('code', ['A1Ticket', 'A1Mascot', 'A2Ticket', 'A3Ticket']),
('code', ['A1Mascot', 'A1Ticket', 'A2Ticket', 'A3Ticket']),
('-email', ['A3Ticket', 'A2Ticket', 'A1Ticket', 'A1Mascot']),
('email', ['A1Ticket', 'A1Mascot', 'A2Ticket', 'A3Ticket']),
('-status', ['A3Ticket', 'A1Ticket', 'A1Mascot', 'A2Ticket']),
('email', ['A1Mascot', 'A1Ticket', 'A2Ticket', 'A3Ticket']),
('-status', ['A3Ticket', 'A1Ticket', 'A2Ticket', 'A1Mascot']),
('status', ['A1Mascot', 'A2Ticket', 'A1Ticket', 'A3Ticket']),
('-timestamp', ['A1Ticket', 'A3Ticket', 'A1Mascot', 'A2Ticket']), # A1 checkin date > A3 checkin date
('-timestamp', ['A1Ticket', 'A3Ticket', 'A2Ticket', 'A1Mascot']), # A1 checkin date > A3 checkin date
('timestamp', ['A1Mascot', 'A2Ticket', 'A3Ticket', 'A1Ticket']),
('-name', ['A3Ticket', 'A2Ticket', 'A1Ticket', 'A1Mascot']),
('name', ['A1Mascot', 'A1Ticket', 'A2Ticket', 'A3Ticket']), # mascot doesn't include attendee name
('-item', ['A1Ticket', 'A2Ticket', 'A3Ticket', 'A1Mascot']),
('-item', ['A3Ticket', 'A2Ticket', 'A1Ticket', 'A1Mascot']),
('item', ['A1Mascot', 'A1Ticket', 'A2Ticket', 'A3Ticket']),
])
def test_checkins_list_ordering(client, checkin_list_env, order_key, expected):
client.login(email='dummy@dummy.dummy', password='dummy')
response = client.get('/control/event/dummy/dummy/checkins/?ordering=' + order_key)
response = client.get('/control/event/dummy/dummy/checkinlists/{}/?ordering='.format(checkin_list_env[6].pk) + order_key)
qs = response.context['entries']
item_keys = [q.order.code + str(q.item.name) for q in qs]
assert item_keys == expected
@@ -240,7 +225,7 @@ def test_checkins_list_ordering(client, checkin_list_env, order_key, expected):
])
def test_checkins_list_filter(client, checkin_list_env, query, expected):
client.login(email='dummy@dummy.dummy', password='dummy')
response = client.get('/control/event/dummy/dummy/checkins/?' + query)
response = client.get('/control/event/dummy/dummy/checkinlists/{}/?'.format(checkin_list_env[6].pk) + query)
qs = response.context['entries']
item_keys = [q.order.code + str(q.item.name) for q in qs]
print([str(item.name) + '-' + str(item.id) for item in Item.objects.all()])
@@ -251,7 +236,7 @@ def test_checkins_list_filter(client, checkin_list_env, query, expected):
def test_checkins_item_filter(client, checkin_list_env):
client.login(email='dummy@dummy.dummy', password='dummy')
for item in checkin_list_env[3]:
response = client.get('/control/event/dummy/dummy/checkins/?item=' + str(item.id))
response = client.get('/control/event/dummy/dummy/checkinlists/{}/?item={}'.format(checkin_list_env[6].pk, item.pk))
assert all(i.item.id == item.id for i in response.context['entries'])
@@ -263,7 +248,7 @@ def test_checkins_item_filter(client, checkin_list_env):
])
def test_checkins_list_mixed(client, checkin_list_env, query, expected):
client.login(email='dummy@dummy.dummy', password='dummy')
response = client.get('/control/event/dummy/dummy/checkins/?' + query)
response = client.get('/control/event/dummy/dummy/checkinlists/{}/?{}'.format(checkin_list_env[6].pk, query))
qs = response.context['entries']
item_keys = [q.order.code + str(q.item.name) for q in qs]
assert item_keys == expected
@@ -273,7 +258,7 @@ def test_checkins_list_mixed(client, checkin_list_env, query, expected):
def test_manual_checkins(client, checkin_list_env):
client.login(email='dummy@dummy.dummy', password='dummy')
assert not checkin_list_env[5][3].checkins.exists()
client.post('/control/event/dummy/dummy/checkins/', {
client.post('/control/event/dummy/dummy/checkinlists/{}/'.format(checkin_list_env[6].pk), {
'checkin': [checkin_list_env[5][3].pk]
})
assert checkin_list_env[5][3].checkins.exists()
@@ -299,6 +284,7 @@ def checkin_list_with_addon_env():
event.settings.set('attendee_names_asked', True)
event.settings.set('locales', ['en', 'de'])
team.limit_events.add(event)
cl = event.checkin_lists.create(name="Default", all_products=True)
# item
cat_adm = ItemCategory.objects.create(event=event, name="Admission")
@@ -359,29 +345,69 @@ def checkin_list_with_addon_env():
)
# checkin
Checkin.objects.create(position=op_a1_ticket, datetime=now() + timedelta(minutes=1))
Checkin.objects.create(position=op_a1_ticket, datetime=now() + timedelta(minutes=1), list=cl)
return event, user, orga, [item_ticket, item_workshop], [order_pending, order_a1, order_a2], \
[op_pending_ticket, op_a1_ticket, op_a1_workshop, op_a2_ticket]
[op_pending_ticket, op_a1_ticket, op_a1_workshop, op_a2_ticket], cl
@pytest.mark.django_db
def test_checkins_attendee_name_from_addon_available(client, checkin_list_with_addon_env):
client.login(email='dummy@dummy.dummy', password='dummy')
response = client.get('/control/event/dummy/dummy/checkins/')
response = client.get('/control/event/dummy/dummy/checkinlists/{}/'.format(checkin_list_with_addon_env[6].pk))
qs = response.context['entries']
item_keys = [q.order.code + str(q.item.name) +
(str(q.addon_to.attendee_name) if q.addon_to is not None else str(q.attendee_name)) for q in qs]
assert item_keys == ['A1TicketA1', 'A1WorkshopA1', 'A2TicketA2'] # A1Workshop<name> comes from addon_to position
@pytest.mark.django_db
def test_checkins_with_noadm_option(client, checkin_list_with_addon_env):
checkin_list_with_addon_env[0].settings.ticket_download_nonadm = False
checkin_list_with_addon_env[0].save()
client.login(email='dummy@dummy.dummy', password='dummy')
response = client.get('/control/event/dummy/dummy/checkins/')
qs = response.context['entries']
item_keys = [q.order.code + str(q.item.name) +
(str(q.addon_to.attendee_name) if q.addon_to is not None else str(q.attendee_name)) for q in qs]
assert item_keys == ['A1TicketA1', 'A2TicketA2']
class CheckinListFormTest(SoupTest):
def setUp(self):
super().setUp()
self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
self.orga1 = Organizer.objects.create(name='CCC', slug='ccc')
self.orga2 = Organizer.objects.create(name='MRM', slug='mrm')
self.event1 = Event.objects.create(
organizer=self.orga1, name='30C3', slug='30c3',
date_from=datetime(2013, 12, 26, tzinfo=timezone.utc),
)
t = Team.objects.create(organizer=self.orga1, can_change_event_settings=True, can_view_orders=True)
t.members.add(self.user)
t.limit_events.add(self.event1)
self.client.login(email='dummy@dummy.dummy', password='dummy')
self.item_ticket = Item.objects.create(event=self.event1, name="Ticket", default_price=23, admission=True)
def test_create(self):
doc = self.get_doc('/control/event/%s/%s/checkinlists/add' % (self.orga1.slug, self.event1.slug))
form_data = extract_form_fields(doc.select('.container-fluid form')[0])
form_data['name'] = 'All'
form_data['all_products'] = 'on'
doc = self.post_doc('/control/event/%s/%s/checkinlists/add' % (self.orga1.slug, self.event1.slug), form_data)
assert doc.select(".alert-success")
self.assertIn("All", doc.select("#page-wrapper table")[0].text)
assert self.event1.checkin_lists.get(
name='All', all_products=True
)
def test_update(self):
cl = self.event1.checkin_lists.create(name='All', all_products=True)
doc = self.get_doc('/control/event/%s/%s/checkinlists/%s/change' % (self.orga1.slug, self.event1.slug, cl.id))
form_data = extract_form_fields(doc.select('.container-fluid form')[0])
form_data['all_products'] = ''
form_data['limit_products'] = str(self.item_ticket.pk)
doc = self.post_doc('/control/event/%s/%s/checkinlists/%s/change' % (self.orga1.slug, self.event1.slug, cl.id),
form_data)
assert doc.select(".alert-success")
cl.refresh_from_db()
assert not cl.all_products
assert list(cl.limit_products.all()) == [self.item_ticket]
def test_delete(self):
cl = self.event1.checkin_lists.create(name='All', all_products=True)
doc = self.get_doc('/control/event/%s/%s/checkinlists/%s/delete' % (self.orga1.slug, self.event1.slug, cl.id))
form_data = extract_form_fields(doc.select('.container-fluid form')[0])
doc = self.post_doc('/control/event/%s/%s/checkinlists/%s/delete' % (self.orga1.slug, self.event1.slug, cl.id),
form_data)
assert doc.select(".alert-success")
self.assertNotIn("VAT", doc.select("#page-wrapper")[0].text)
assert not self.event1.checkin_lists.exists()

View File

@@ -613,6 +613,12 @@ class SubEventsTest(SoupTest):
'location_0': 'Hamburg',
'presale_start_0': '2017-06-20',
'presale_start_1': '10:00:00',
'checkinlist_set-TOTAL_FORMS': '1',
'checkinlist_set-INITIAL_FORMS': '0',
'checkinlist_set-MIN_NUM_FORMS': '0',
'checkinlist_set-MAX_NUM_FORMS': '1000',
'checkinlist_set-0-name': 'Default',
'checkinlist_set-0-all_products': 'on',
'quotas-TOTAL_FORMS': '1',
'quotas-INITIAL_FORMS': '0',
'quotas-MIN_NUM_FORMS': '0',
@@ -638,6 +644,7 @@ class SubEventsTest(SoupTest):
assert list(q.items.all()) == [self.ticket]
sei = SubEventItem.objects.get(subevent=se, item=self.ticket)
assert sei.price == 12
assert se.checkinlist_set.count() == 1
def test_modify(self):
doc = self.get_doc('/control/event/ccc/30c3/subevents/%d/' % self.subevent1.pk)
@@ -659,8 +666,15 @@ class SubEventsTest(SoupTest):
'quotas-0-name': 'Q1',
'quotas-0-size': '50',
'quotas-0-itemvars': str(self.ticket.pk),
'checkinlist_set-TOTAL_FORMS': '1',
'checkinlist_set-INITIAL_FORMS': '0',
'checkinlist_set-MIN_NUM_FORMS': '0',
'checkinlist_set-MAX_NUM_FORMS': '1000',
'checkinlist_set-0-name': 'Default',
'checkinlist_set-0-all_products': 'on',
'item-%d-price' % self.ticket.pk: '12'
})
print(doc)
assert doc.select(".alert-success")
self.subevent1.refresh_from_db()
se = self.subevent1
@@ -678,6 +692,7 @@ class SubEventsTest(SoupTest):
assert list(q.items.all()) == [self.ticket]
sei = SubEventItem.objects.get(subevent=se, item=self.ticket)
assert sei.price == 12
assert se.checkinlist_set.count() == 1
def test_delete(self):
doc = self.get_doc('/control/event/ccc/30c3/subevents/%d/delete' % self.subevent1.pk)

View File

@@ -89,6 +89,10 @@ event_urls = [
"orders/ABC/checkvatid",
"orders/ABC/",
"orders/",
"checkinlists/",
"checkinlists/1/",
"checkinlists/1/change",
"checkinlists/1/delete",
"waitinglist/",
"waitinglist/auto_assign",
"invoice/1",
@@ -225,7 +229,11 @@ event_permission_urls = [
("can_change_vouchers", "vouchers/1234/delete", 404),
("can_view_orders", "waitinglist/", 200),
("can_change_orders", "waitinglist/auto_assign", 405),
("can_view_orders", "checkins/", 200),
("can_view_orders", "checkinlists/", 200),
("can_view_orders", "checkinlists/1/", 404),
("can_change_event_settings", "checkinlists/add", 200),
("can_change_event_settings", "checkinlists/1/change", 404),
("can_change_event_settings", "checkinlists/1/delete", 404),
]

View File

@@ -42,12 +42,14 @@ def env():
order=o1, item=ticket,
price=23, attendee_name="Peter", secret='5678910'
)
return event, user, o1, op1, op2
cl1 = event.checkin_lists.create(name="Foo", all_products=True)
cl2 = event.checkin_lists.create(name="Bar", all_products=True)
return event, user, o1, op1, op2, cl1, cl2
@pytest.mark.django_db
def test_custom_datetime(client, env):
AppConfiguration.objects.create(event=env[0], key='abcdefg')
AppConfiguration.objects.create(event=env[0], key='abcdefg', list=env[5])
dt = now() - timedelta(days=1)
dt = dt.replace(microsecond=0)
resp = client.post('/pretixdroid/api/%s/%s/redeem/?key=%s' % (env[0].organizer.slug, env[0].slug, 'abcdefg'),
@@ -60,7 +62,7 @@ def test_custom_datetime(client, env):
@pytest.mark.django_db
def test_only_once(client, env):
AppConfiguration.objects.create(event=env[0], key='abcdefg')
AppConfiguration.objects.create(event=env[0], key='abcdefg', list=env[5])
resp = client.post('/pretixdroid/api/%s/%s/redeem/?key=%s' % (env[0].organizer.slug, env[0].slug, 'abcdefg'),
data={'secret': '1234'})
@@ -76,7 +78,7 @@ def test_only_once(client, env):
@pytest.mark.django_db
def test_item_scope(client, env):
ac = AppConfiguration.objects.create(event=env[0], key='abcdefg', all_items=False)
ac = AppConfiguration.objects.create(event=env[0], key='abcdefg', all_items=False, list=env[5])
ac.items.add(env[4].item)
resp = client.post('/pretixdroid/api/%s/%s/redeem/?key=%s' % (env[0].organizer.slug, env[0].slug, 'abcdefg'),
@@ -91,9 +93,28 @@ def test_item_scope(client, env):
assert jdata['reason'] == 'product'
@pytest.mark.django_db
def test_item_restricted_list(client, env):
AppConfiguration.objects.create(event=env[0], key='abcdefg', all_items=True, list=env[5])
env[5].all_products = False
env[5].limit_products.add(env[4].item)
env[5].save()
resp = client.post('/pretixdroid/api/%s/%s/redeem/?key=%s' % (env[0].organizer.slug, env[0].slug, 'abcdefg'),
data={'secret': env[4].secret})
jdata = json.loads(resp.content.decode("utf-8"))
assert jdata['version'] == API_VERSION
assert jdata['status'] == 'ok'
resp = client.post('/pretixdroid/api/%s/%s/redeem/?key=%s' % (env[0].organizer.slug, env[0].slug, 'abcdefg'),
data={'secret': env[3].secret})
jdata = json.loads(resp.content.decode("utf-8"))
assert jdata['status'] == 'error'
assert jdata['reason'] == 'product'
@pytest.mark.django_db
def test_reupload_same_nonce(client, env):
AppConfiguration.objects.create(event=env[0], key='abcdefg')
AppConfiguration.objects.create(event=env[0], key='abcdefg', list=env[5])
resp = client.post('/pretixdroid/api/%s/%s/redeem/?key=%s' % (env[0].organizer.slug, env[0].slug, 'abcdefg'),
data={'secret': '1234', 'nonce': 'fooobar'})
@@ -107,9 +128,27 @@ def test_reupload_same_nonce(client, env):
assert Checkin.objects.count() == 1
@pytest.mark.django_db
def test_multiple_different_list(client, env):
ac = AppConfiguration.objects.create(event=env[0], key='abcdefg', list=env[5])
resp = client.post('/pretixdroid/api/%s/%s/redeem/?key=%s' % (env[0].organizer.slug, env[0].slug, 'abcdefg'),
data={'secret': '1234'})
jdata = json.loads(resp.content.decode("utf-8"))
assert jdata['version'] == API_VERSION
assert jdata['status'] == 'ok'
ac.list = env[6]
ac.save()
resp = client.post('/pretixdroid/api/%s/%s/redeem/?key=%s' % (env[0].organizer.slug, env[0].slug, 'abcdefg'),
data={'secret': '1234'})
jdata = json.loads(resp.content.decode("utf-8"))
assert jdata['status'] == 'ok'
@pytest.mark.django_db
def test_forced_multiple(client, env):
AppConfiguration.objects.create(event=env[0], key='abcdefg')
AppConfiguration.objects.create(event=env[0], key='abcdefg', list=env[5])
resp = client.post('/pretixdroid/api/%s/%s/redeem/?key=%s' % (env[0].organizer.slug, env[0].slug, 'abcdefg'),
data={'secret': '1234'})
@@ -124,7 +163,7 @@ def test_forced_multiple(client, env):
@pytest.mark.django_db
def test_require_paid(client, env):
AppConfiguration.objects.create(event=env[0], key='abcdefg')
AppConfiguration.objects.create(event=env[0], key='abcdefg', list=env[5])
env[2].status = Order.STATUS_PENDING
env[2].save()
@@ -137,7 +176,7 @@ def test_require_paid(client, env):
@pytest.mark.django_db
def test_unknown(client, env):
AppConfiguration.objects.create(event=env[0], key='abcdefg')
AppConfiguration.objects.create(event=env[0], key='abcdefg', list=env[5])
resp = client.post('/pretixdroid/api/%s/%s/redeem/?key=%s' % (env[0].organizer.slug, env[0].slug, 'abcdefg'),
data={'secret': '4321'})
@@ -162,7 +201,7 @@ def test_unknown_event(client, env):
@pytest.mark.django_db
def test_search(client, env):
AppConfiguration.objects.create(event=env[0], key='abcdefg')
AppConfiguration.objects.create(event=env[0], key='abcdefg', list=env[5])
resp = client.get('/pretixdroid/api/%s/%s/search/?key=%s&query=%s' % (
env[0].organizer.slug, env[0].slug, 'abcdefg', '567891'))
jdata = json.loads(resp.content.decode("utf-8"))
@@ -175,9 +214,28 @@ def test_search(client, env):
assert jdata['results'][0]['secret'] == '5678910'
@pytest.mark.django_db
def test_search_item_restricted_list(client, env):
AppConfiguration.objects.create(event=env[0], key='abcdefg', list=env[5])
env[5].all_products = False
env[5].limit_products.add(env[4].item)
env[5].save()
resp = client.get('/pretixdroid/api/%s/%s/search/?key=%s&query=%s' % (
env[0].organizer.slug, env[0].slug, 'abcdefg', '567891'))
jdata = json.loads(resp.content.decode("utf-8"))
assert len(jdata['results']) == 1
assert jdata['results'][0]['secret'] == env[4].secret
env[5].limit_products.remove(env[4].item)
resp = client.get('/pretixdroid/api/%s/%s/search/?key=%s&query=%s' % (
env[0].organizer.slug, env[0].slug, 'abcdefg', '567891'))
jdata = json.loads(resp.content.decode("utf-8"))
assert len(jdata['results']) == 0
@pytest.mark.django_db
def test_search_restricted(client, env):
AppConfiguration.objects.create(event=env[0], key='abcdefg', allow_search=False)
AppConfiguration.objects.create(event=env[0], key='abcdefg', list=env[5], allow_search=False)
resp = client.get('/pretixdroid/api/%s/%s/search/?key=%s&query=%s' % (
env[0].organizer.slug, env[0].slug, 'abcdefg', '567891'))
jdata = json.loads(resp.content.decode("utf-8"))
@@ -191,7 +249,7 @@ def test_search_restricted(client, env):
@pytest.mark.django_db
def test_search_invoice_name(client, env):
AppConfiguration.objects.create(event=env[0], key='abcdefg')
AppConfiguration.objects.create(event=env[0], key='abcdefg', list=env[5])
InvoiceAddress.objects.create(order=env[2], name="John")
resp = client.get('/pretixdroid/api/%s/%s/search/?key=%s&query=%s' % (
env[0].organizer.slug, env[0].slug, 'abcdefg', 'John'))
@@ -202,7 +260,7 @@ def test_search_invoice_name(client, env):
@pytest.mark.django_db
def test_download_all_data(client, env):
AppConfiguration.objects.create(event=env[0], key='abcdefg')
AppConfiguration.objects.create(event=env[0], key='abcdefg', list=env[5])
resp = client.get('/pretixdroid/api/%s/%s/download/?key=%s' % (env[0].organizer.slug, env[0].slug, 'abcdefg'))
jdata = json.loads(resp.content.decode("utf-8"))
assert len(jdata['results']) == 2
@@ -212,7 +270,7 @@ def test_download_all_data(client, env):
@pytest.mark.django_db
def test_download_item_restriction(client, env):
ac = AppConfiguration.objects.create(event=env[0], key='abcdefg', all_items=False)
ac = AppConfiguration.objects.create(event=env[0], key='abcdefg', list=env[5], all_items=False)
ac.items.add(env[4].item)
resp = client.get('/pretixdroid/api/%s/%s/download/?key=%s' % (env[0].organizer.slug, env[0].slug, 'abcdefg'))
jdata = json.loads(resp.content.decode("utf-8"))
@@ -220,10 +278,22 @@ def test_download_item_restriction(client, env):
assert jdata['results'][0]['secret'] == env[4].secret
@pytest.mark.django_db
def test_download_item_restricted_list(client, env):
AppConfiguration.objects.create(event=env[0], key='abcdefg', all_items=True, list=env[5])
env[5].all_products = False
env[5].limit_products.add(env[4].item)
env[5].save()
resp = client.get('/pretixdroid/api/%s/%s/download/?key=%s' % (env[0].organizer.slug, env[0].slug, 'abcdefg'))
jdata = json.loads(resp.content.decode("utf-8"))
assert len(jdata['results']) == 1
assert jdata['results'][0]['secret'] == env[4].secret
@pytest.mark.django_db
def test_status(client, env):
AppConfiguration.objects.create(event=env[0], key='abcdefg')
Checkin.objects.create(position=env[3])
AppConfiguration.objects.create(event=env[0], key='abcdefg', list=env[5])
Checkin.objects.create(position=env[3], list=env[5])
resp = client.get('/pretixdroid/api/%s/%s/status/?key=%s' % (
env[0].organizer.slug, env[0].slug, 'abcdefg'))
jdata = json.loads(resp.content.decode("utf-8"))

View File

@@ -45,12 +45,14 @@ def env():
order=o1, item=ticket,
price=23, attendee_name="Peter", secret='5678910', subevent=se2
)
return event, user, o1, op1, op2, se1, se2
cl1 = event.checkin_lists.create(name="Foo", all_products=True, subevent=se1)
cl2 = event.checkin_lists.create(name="Foo", all_products=True, subevent=se2)
return event, user, o1, op1, op2, se1, se2, cl1, cl2
@pytest.mark.django_db
def test_custom_datetime(client, env):
AppConfiguration.objects.create(event=env[0], key='abcdefg', subevent=env[5])
AppConfiguration.objects.create(event=env[0], key='abcdefg', list=env[7])
dt = now() - timedelta(days=1)
dt = dt.replace(microsecond=0)
resp = client.post('/pretixdroid/api/%s/%s/%d/redeem/?key=%s' % (
@@ -64,14 +66,12 @@ def test_custom_datetime(client, env):
@pytest.mark.django_db
def test_wrong_subevent(client, env):
AppConfiguration.objects.create(event=env[0], key='abcdefg')
AppConfiguration.objects.create(event=env[0], key='abcdefg', list=env[8])
resp = client.post('/pretixdroid/api/%s/%s/%d/redeem/?key=%s' % (
env[0].organizer.slug, env[0].slug, env[5].pk, 'abcdefg'
), data={'secret': '5678910'})
jdata = json.loads(resp.content.decode("utf-8"))
assert jdata['status'] == 'error'
assert jdata['reason'] == 'unknown_ticket'
assert resp.status_code == 403
resp = client.post('/pretixdroid/api/%s/%s/%d/redeem/?key=%s' % (
env[0].organizer.slug, env[0].slug, env[6].pk, 'abcdefg'
@@ -82,17 +82,14 @@ def test_wrong_subevent(client, env):
@pytest.mark.django_db
def test_other_subevent_not_allowed(client, env):
ac = AppConfiguration.objects.create(event=env[0], key='abcdefg', subevent=env[5])
AppConfiguration.objects.create(event=env[0], key='abcdefg', list=env[7])
resp = client.post('/pretixdroid/api/%s/%s/%d/redeem/?key=%s' % (
env[0].organizer.slug, env[0].slug, env[6].pk, 'abcdefg'
), data={'secret': '5678910'})
jdata = json.loads(resp.content.decode("utf-8"))
assert jdata['status'] == 'error'
assert jdata['reason'] == 'unknown_ticket'
assert resp.status_code == 403
ac.subevent = env[6]
ac.save()
env[7].subevent = env[6]
env[7].save()
resp = client.post('/pretixdroid/api/%s/%s/%d/redeem/?key=%s' % (
env[0].organizer.slug, env[0].slug, env[6].pk, 'abcdefg'
@@ -103,11 +100,11 @@ def test_other_subevent_not_allowed(client, env):
@pytest.mark.django_db
def test_unknown_subevent(client, env):
AppConfiguration.objects.create(event=env[0], key='abcdefg')
AppConfiguration.objects.create(event=env[0], key='abcdefg', list=env[7])
resp = client.post('/pretixdroid/api/%s/%s/%d/redeem/?key=%s' % (
env[0].organizer.slug, env[0].slug, env[6].pk + 1000, 'abcdefg'
), data={'secret': '5678910'})
assert resp.status_code == 404
assert resp.status_code == 403
@pytest.mark.django_db
@@ -120,9 +117,10 @@ def test_no_subevent(client, env):
@pytest.mark.django_db
def test_search(client, env):
AppConfiguration.objects.create(event=env[0], key='abcdefg')
AppConfiguration.objects.create(event=env[0], key='hijklmn', list=env[7])
AppConfiguration.objects.create(event=env[0], key='abcdefg', list=env[8])
resp = client.get('/pretixdroid/api/%s/%s/%d/search/?key=%s&query=%s' % (
env[0].organizer.slug, env[0].slug, env[5].pk, 'abcdefg', '567891'))
env[0].organizer.slug, env[0].slug, env[5].pk, 'hijklmn', '567891'))
jdata = json.loads(resp.content.decode("utf-8"))
assert len(jdata['results']) == 0
resp = client.get('/pretixdroid/api/%s/%s/%d/search/?key=%s&query=%s' % (
@@ -134,7 +132,7 @@ def test_search(client, env):
@pytest.mark.django_db
def test_download_all_data(client, env):
AppConfiguration.objects.create(event=env[0], key='abcdefg', subevent=env[5])
AppConfiguration.objects.create(event=env[0], key='abcdefg', list=env[7])
resp = client.get('/pretixdroid/api/%s/%s/%d/download/?key=%s' % (
env[0].organizer.slug, env[0].slug, env[5].pk, 'abcdefg'))
jdata = json.loads(resp.content.decode("utf-8"))
@@ -144,8 +142,8 @@ def test_download_all_data(client, env):
@pytest.mark.django_db
def test_status(client, env):
AppConfiguration.objects.create(event=env[0], key='abcdefg', subevent=env[5])
Checkin.objects.create(position=env[3])
AppConfiguration.objects.create(event=env[0], key='abcdefg', list=env[7])
Checkin.objects.create(position=env[3], list=env[7])
resp = client.get('/pretixdroid/api/%s/%s/%d/status/?key=%s' % (
env[0].organizer.slug, env[0].slug, env[5].pk, 'abcdefg'))
jdata = json.loads(resp.content.decode("utf-8"))