forked from CGM_Public/pretix_original
* 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:
37
src/pretix/api/serializers/checkin.py
Normal file
37
src/pretix/api/serializers/checkin.py
Normal 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
|
||||
@@ -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 %}
|
||||
@@ -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():
|
||||
|
||||
59
src/pretix/api/views/checkin.py
Normal file
59
src/pretix/api/views/checkin.py
Normal 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)
|
||||
106
src/pretix/base/migrations/0077_auto_20171124_1629.py
Normal file
106
src/pretix/base/migrations/0077_auto_20171124_1629.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
31
src/pretix/control/forms/checkin.py
Normal file
31
src/pretix/control/forms/checkin.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from django import forms
|
||||
|
||||
from pretix.base.models.checkin import CheckinList
|
||||
|
||||
|
||||
class CheckinListForm(forms.ModelForm):
|
||||
def __init__(self, **kwargs):
|
||||
self.event = kwargs.pop('event')
|
||||
kwargs.pop('locales', None)
|
||||
super().__init__(**kwargs)
|
||||
self.fields['limit_products'].queryset = self.event.items.all()
|
||||
if self.event.has_subevents:
|
||||
self.fields['subevent'].queryset = self.event.subevents.all()
|
||||
self.fields['subevent'].required = True
|
||||
else:
|
||||
del self.fields['subevent']
|
||||
|
||||
class Meta:
|
||||
model = CheckinList
|
||||
localized_fields = '__all__'
|
||||
fields = [
|
||||
'name',
|
||||
'all_products',
|
||||
'limit_products',
|
||||
'subevent'
|
||||
]
|
||||
widgets = {
|
||||
'limit_products': forms.CheckboxSelectMultiple(attrs={
|
||||
'data-inverse-dependency': '<[name$=all_products]'
|
||||
}),
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
from django import forms
|
||||
from django.apps import apps
|
||||
from django.db.models import Exists, OuterRef, Q
|
||||
from django.db.models.functions import Concat
|
||||
from django.db.models import Exists, F, OuterRef, Q
|
||||
from django.db.models.functions import Coalesce, Concat
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
|
||||
from pretix.base.models import Event, Invoice, Item, Order, Organizer, SubEvent
|
||||
from pretix.base.signals import register_payment_providers
|
||||
from pretix.control.utils.i18n import i18ncomp
|
||||
from pretix.helpers.database import rolledback_transaction
|
||||
from pretix.helpers.database import FixedOrderBy, rolledback_transaction
|
||||
|
||||
PAYMENT_PROVIDERS = []
|
||||
|
||||
@@ -57,7 +57,10 @@ class FilterForm(forms.Form):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['ordering'] = forms.ChoiceField(
|
||||
choices=sum([[(a, b), ('-' + a, '-' + b)] for a, b in self.orders.items()], []),
|
||||
choices=sum([
|
||||
[(a, a), ('-' + a, '-' + a)]
|
||||
for a in self.orders.keys()
|
||||
], []),
|
||||
required=False
|
||||
)
|
||||
|
||||
@@ -136,7 +139,7 @@ class OrderFilterForm(FilterForm):
|
||||
qs = qs.filter(status=s)
|
||||
|
||||
if fdata.get('ordering'):
|
||||
qs = qs.order_by(dict(self.fields['ordering'].choices)[fdata.get('ordering')])
|
||||
qs = qs.order_by(self.orders[fdata.get('ordering')])
|
||||
|
||||
if fdata.get('provider'):
|
||||
qs = qs.filter(payment_provider=fdata.get('provider'))
|
||||
@@ -273,7 +276,7 @@ class SubEventFilterForm(FilterForm):
|
||||
)
|
||||
|
||||
if fdata.get('ordering'):
|
||||
qs = qs.order_by(dict(self.fields['ordering'].choices)[fdata.get('ordering')])
|
||||
qs = qs.order_by(self.orders[fdata.get('ordering')])
|
||||
|
||||
return qs
|
||||
|
||||
@@ -387,6 +390,94 @@ class EventFilterForm(FilterForm):
|
||||
)
|
||||
|
||||
if fdata.get('ordering'):
|
||||
qs = qs.order_by(dict(self.fields['ordering'].choices)[fdata.get('ordering')])
|
||||
qs = qs.order_by(self.orders[fdata.get('ordering')])
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
class CheckInFilterForm(FilterForm):
|
||||
orders = {
|
||||
'code': ('order__code', 'item__name'),
|
||||
'-code': ('-order__code', '-item__name'),
|
||||
'email': ('order__email', 'item__name'),
|
||||
'-email': ('-order__email', '-item__name'),
|
||||
'status': (FixedOrderBy(F('last_checked_in'), nulls_first=True, descending=True), 'order__code'),
|
||||
'-status': (FixedOrderBy(F('last_checked_in'), nulls_last=True), '-order__code'),
|
||||
'timestamp': (FixedOrderBy(F('last_checked_in'), nulls_first=True), 'order__code'),
|
||||
'-timestamp': (FixedOrderBy(F('last_checked_in'), nulls_last=True, descending=True), '-order__code'),
|
||||
'item': ('item__name', 'variation__value', 'order__code'),
|
||||
'-item': ('-item__name', '-variation__value', '-order__code'),
|
||||
'name': {'_order': F('display_name').asc(nulls_first=True),
|
||||
'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')},
|
||||
'-name': {'_order': F('display_name').desc(nulls_last=True),
|
||||
'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')},
|
||||
}
|
||||
|
||||
user = forms.CharField(
|
||||
label=_('Search attendee…'),
|
||||
widget=forms.TextInput(attrs={
|
||||
'placeholder': _('Search attendee…'),
|
||||
'autofocus': 'autofocus'
|
||||
}),
|
||||
required=False
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
label=_('Check-in status'),
|
||||
choices=(
|
||||
('', _('All attendees')),
|
||||
('1', _('Checked in')),
|
||||
('0', _('Not checked in')),
|
||||
),
|
||||
required=False,
|
||||
)
|
||||
item = forms.ModelChoiceField(
|
||||
label=_('Products'),
|
||||
queryset=Item.objects.none(),
|
||||
required=False,
|
||||
empty_label=_('All products')
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.event = kwargs.pop('event')
|
||||
self.list = kwargs.pop('list')
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.list.all_products:
|
||||
self.fields['item'].queryset = self.event.items.all()
|
||||
else:
|
||||
self.fields['item'].queryset = self.list.limit_products.all()
|
||||
|
||||
def filter_qs(self, qs):
|
||||
fdata = self.cleaned_data
|
||||
|
||||
if fdata.get('user'):
|
||||
u = fdata.get('user')
|
||||
qs = qs.filter(
|
||||
Q(order__email__icontains=u)
|
||||
| Q(attendee_name__icontains=u)
|
||||
| Q(attendee_email__icontains=u)
|
||||
| Q(order__invoice_address__name__icontains=u)
|
||||
| Q(order__invoice_address__company__icontains=u)
|
||||
)
|
||||
|
||||
if fdata.get('status'):
|
||||
s = fdata.get('status')
|
||||
if s == '1':
|
||||
qs = qs.filter(last_checked_in__isnull=False)
|
||||
elif s == '0':
|
||||
qs = qs.filter(last_checked_in__isnull=True)
|
||||
|
||||
if fdata.get('ordering'):
|
||||
ob = self.orders[fdata.get('ordering')]
|
||||
if isinstance(ob, dict):
|
||||
ob = dict(ob)
|
||||
o = ob.pop('_order')
|
||||
qs = qs.annotate(**ob).order_by(o)
|
||||
elif isinstance(ob, (list, tuple)):
|
||||
qs = qs.order_by(*ob)
|
||||
else:
|
||||
qs = qs.order_by(ob)
|
||||
|
||||
if fdata.get('item'):
|
||||
qs = qs.filter(item=fdata.get('item'))
|
||||
|
||||
return qs
|
||||
|
||||
@@ -123,3 +123,30 @@ class SubEventMetaValueForm(forms.ModelForm):
|
||||
widgets = {
|
||||
'value': forms.TextInput
|
||||
}
|
||||
|
||||
|
||||
class CheckinListFormSet(I18nInlineFormSet):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.event = kwargs.pop('event', None)
|
||||
self.locales = self.event.settings.get('locales')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@cached_property
|
||||
def items(self):
|
||||
return self.event.items.prefetch_related('variations').all()
|
||||
|
||||
def _construct_form(self, i, **kwargs):
|
||||
kwargs['event'] = self.event
|
||||
return super()._construct_form(i, **kwargs)
|
||||
|
||||
@property
|
||||
def empty_form(self):
|
||||
form = self.form(
|
||||
auto_id=self.auto_id,
|
||||
prefix=self.add_prefix('__prefix__'),
|
||||
empty_permitted=True,
|
||||
event=self.event,
|
||||
)
|
||||
self.add_fields(form, None)
|
||||
return form
|
||||
|
||||
@@ -9,7 +9,9 @@ from django.utils.formats import date_format
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.base.models import Event, ItemVariation, LogEntry, OrderPosition
|
||||
from pretix.base.models import (
|
||||
CheckinList, Event, ItemVariation, LogEntry, OrderPosition,
|
||||
)
|
||||
from pretix.base.signals import logentry_display
|
||||
|
||||
OVERVIEW_BLACKLIST = [
|
||||
@@ -167,6 +169,9 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'pretix.event.taxrule.added': _('The tax rule has been added.'),
|
||||
'pretix.event.taxrule.deleted': _('The tax rule has been deleted.'),
|
||||
'pretix.event.taxrule.changed': _('The tax rule has been changed.'),
|
||||
'pretix.event.checkinlist.added': _('The check-in list has been added.'),
|
||||
'pretix.event.checkinlist.deleted': _('The check-in list has been deleted.'),
|
||||
'pretix.event.checkinlist.changed': _('The check-in list has been changed.'),
|
||||
'pretix.event.settings': _('The event settings have been changed.'),
|
||||
'pretix.event.tickets.settings': _('The ticket download settings have been changed.'),
|
||||
'pretix.event.plugins.enabled': _('A plugin has been enabled.'),
|
||||
@@ -222,15 +227,24 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
dt = dateutil.parser.parse(data.get('datetime'))
|
||||
tz = pytz.timezone(sender.settings.timezone)
|
||||
dt_formatted = date_format(dt.astimezone(tz), "SHORT_DATETIME_FORMAT")
|
||||
if 'list' in data:
|
||||
try:
|
||||
checkin_list = sender.checkin_lists.get(pk=data.get('list')).name
|
||||
except CheckinList.DoesNotExist:
|
||||
checkin_list = _("(unknown)")
|
||||
else:
|
||||
checkin_list = _("(unknown)")
|
||||
|
||||
if data.get('first'):
|
||||
return _('Position #{posid} has been checked in manually at {datetime}.').format(
|
||||
return _('Position #{posid} has been checked in manually at {datetime} on list "{list}".').format(
|
||||
posid=data.get('positionid'),
|
||||
datetime=dt_formatted
|
||||
datetime=dt_formatted,
|
||||
list=checkin_list,
|
||||
)
|
||||
return _('Position #{posid} has been checked in again at {datetime}.').format(
|
||||
return _('Position #{posid} has been checked in again at {datetime} on list "{list}".').format(
|
||||
posid=data.get('positionid'),
|
||||
datetime=dt_formatted
|
||||
datetime=dt_formatted,
|
||||
list=checkin_list
|
||||
)
|
||||
|
||||
if logentry.action_type == 'pretix.team.member.added':
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/sb-admin-2.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/main.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/quota.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/subevent.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/question.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/mail.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/typeahead.js" %}"></script>
|
||||
|
||||
@@ -2,65 +2,70 @@
|
||||
{% load i18n %}
|
||||
{% load eventurl %}
|
||||
{% load urlreplace %}
|
||||
{% block title %}{% trans "Check-ins" %}{% endblock %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}
|
||||
{% blocktrans with name=checkinlist.name %}Check-in list: {{ name }}{% endblocktrans %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Check-ins" %}</h1>
|
||||
<p>
|
||||
<form class="form-inline helper-display-inline" action="" method="get">
|
||||
<select name="status" class="form-control">
|
||||
<option value="">{% trans "All status" %}</option>
|
||||
<option value="1" {% if request.GET.status == "1" %}selected="selected"{% endif %}>{% trans "Checked in" %}</option>
|
||||
<option value="0" {% if request.GET.status == "0" %}selected="selected"{% endif %}>{% trans "Not checked in" %}</option>
|
||||
</select>
|
||||
<select name="item" class="form-control">
|
||||
<option value="">{% trans "All products" %}</option>
|
||||
{% for item in items %}
|
||||
<option value="{{ item.id }}"
|
||||
{% if request.GET.item|add:0 == item.id %}selected="selected"{% endif %}>
|
||||
{{ item.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% if request.event.has_subevents %}
|
||||
<select name="subevent" class="form-control">
|
||||
<option value="">{% trans "All dates" context "subevent" %}</option>
|
||||
{% for se in request.event.subevents.all %}
|
||||
<option value="{{ se.id }}"
|
||||
{% if request.GET.subevent|add:0 == se.id %}selected="selected"{% endif %}>
|
||||
{{ se.name }} – {{ se.get_date_range_display }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% endif %}
|
||||
<input type="text" name="user" class="form-control" placeholder="{% trans "Search user" %}" value="{{ request.GET.user }}">
|
||||
<button class="btn btn-primary" type="submit">{% trans "Filter" %}</button>
|
||||
</form>
|
||||
</p>
|
||||
<h1>
|
||||
{% blocktrans with name=checkinlist.name %}Check-in list: {{ name }}{% endblocktrans %}
|
||||
{% if 'can_change_event_settings' in request.eventpermset %}
|
||||
<a href="{% url "control:event.orders.checkinlists.edit" event=request.event.slug organizer=request.event.organizer.slug list=checkinlist.pk %}"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-edit"></span>
|
||||
{% trans "Edit list" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url "control:event.orders.export" event=request.event.slug organizer=request.event.organizer.slug %}?identifier=checkinlistpdf&checkinlistpdf-list={{ checkinlist.pk }}"
|
||||
class="btn btn-default" target="_blank">
|
||||
<span class="fa fa-download"></span>
|
||||
{% trans "PDF" %}
|
||||
</a>
|
||||
<a href="{% url "control:event.orders.export" event=request.event.slug organizer=request.event.organizer.slug %}?identifier=checkinlistcsv&checkinlistcsv-list={{ checkinlist.pk }}"
|
||||
class="btn btn-default" target="_blank">
|
||||
<span class="fa fa-download"></span>
|
||||
{% trans "CSV" %}
|
||||
</a>
|
||||
</h1>
|
||||
<form class="row filter-form" action="" method="get">
|
||||
<div class="col-md-4 col-sm-6 col-xs-12">
|
||||
{% bootstrap_field filter_form.user layout='inline' %}
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6 col-xs-12">
|
||||
{% bootstrap_field filter_form.status layout='inline' %}
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6 col-xs-12">
|
||||
{% bootstrap_field filter_form.item layout='inline' %}
|
||||
</div>
|
||||
<div class="col-md-2 col-sm-6 col-xs-12">
|
||||
<button class="btn btn-primary btn-block" type="submit">
|
||||
<span class="fa fa-filter"></span>
|
||||
<span class="hidden-md">
|
||||
{% trans "Filter" %}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% if entries|length == 0 %}
|
||||
<div class="empty-collection">
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
No check-in record was found.
|
||||
No attendee record was found.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
</div>
|
||||
{% else %}
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
<form method="post" action="">
|
||||
{% csrf_token %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th />
|
||||
<th></th>
|
||||
<th>{% trans "Order code" %} <a href="?{% url_replace request 'ordering' '-code'%}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'code'%}"><i class="fa fa-caret-up"></i></a></th>
|
||||
<th>{% trans "Item" %} <a href="?{% url_replace request 'ordering' '-item'%}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'item'%}"><i class="fa fa-caret-up"></i></a></th>
|
||||
{% if request.event.has_subevents %}
|
||||
<th>{% trans "Date" context "subevent" %} <a href="?{% url_replace request 'ordering' '-subevent'%}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'subevent'%}"><i class="fa fa-caret-up"></i></a></th>
|
||||
{% endif %}
|
||||
<th>{% trans "Email" %} <a href="?{% url_replace request 'ordering' '-email'%}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'email'%}"><i class="fa fa-caret-up"></i></a></th>
|
||||
<th>{% trans "Name" %} <a href="?{% url_replace request 'ordering' '-name'%}"><i class="fa fa-caret-down"></i></a>
|
||||
@@ -73,20 +78,16 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for e in entries %}
|
||||
{% with e.checkins.first as checkin %}
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" name="checkin"
|
||||
id="id_checkin" class=""
|
||||
value="{{e.pk}}"/>
|
||||
</td>
|
||||
<td>
|
||||
{% if "can_change_orders" in request.eventpermset %}
|
||||
<input type="checkbox" name="checkin" id="id_checkin" class="" value="{{ e.pk }}"/>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<strong><a href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=e.order.code %}">{{ e.order.code }}</a></strong>
|
||||
</td>
|
||||
<td>{{ e.item.name }}{% if e.variation %} – {{ e.variation }}{% endif %}</td>
|
||||
{% if request.event.has_subevents %}
|
||||
<td>{{ e.subevent.name }} – {{ e.subevent.get_date_range_display }}</td>
|
||||
{% endif %}
|
||||
<td>{{ e.order.email }}</td>
|
||||
<td>
|
||||
{% if e.addon_to %}
|
||||
@@ -96,26 +97,27 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if not checkin %}
|
||||
{% if not e.last_checked_in %}
|
||||
<span class="label label-danger">{% trans "Not checked in" %}</span>
|
||||
{% else %}
|
||||
<span class="label label-success">{% trans "Checked in" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if checkin %}
|
||||
{{ checkin.datetime|date:"SHORT_DATETIME_FORMAT" }}
|
||||
{% if e.last_checked_in %}
|
||||
{{ e.last_checked_in_aware|date:"SHORT_DATETIME_FORMAT" }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Check-In selected attendees" %}
|
||||
</button>
|
||||
{% if "can_change_orders" in request.eventpermset %}
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Check-In selected attendees" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
{% endif %}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Delete check-in list" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Delete check-in list" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<p>{% blocktrans with name=checkinlist.name %}Are you sure you want to delete the check-in list <strong>{{ name }}</strong>?{% endblocktrans %}</p>
|
||||
{% if checkinlist.checkins.exists > 0 %}
|
||||
<p>{% blocktrans with num=checkinlist.checkins.count %}
|
||||
This will delete the information of <strong>{{ num }}</strong> check-ins as well.
|
||||
{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
<div class="form-group submit-group">
|
||||
<a href="{% url "control:event.orders.checkinlists" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-default btn-cancel">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-danger btn-save">
|
||||
{% trans "Delete" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,41 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}
|
||||
{% if checkinlist %}
|
||||
{% blocktrans with name=checkinlist.name %}Check-in list: {{ name }}{% endblocktrans %}
|
||||
{% else %}
|
||||
{% trans "Check-in list" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block inside %}
|
||||
{% if checkinlist %}
|
||||
<h1>{% blocktrans with name=checkinlist.name %}Check-in list: {{ name }}{% endblocktrans %}</h1>
|
||||
{% else %}
|
||||
<h1>{% trans "Check-in list" %}</h1>
|
||||
{% endif %}
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
<fieldset>
|
||||
<legend>{% trans "General information" %}</legend>
|
||||
{% bootstrap_field form.name layout="control" %}
|
||||
{% if form.subevent %}
|
||||
{% bootstrap_field form.subevent layout="control" %}
|
||||
{% endif %}
|
||||
<legend>{% trans "Products" %}</legend>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Please select the products or product variations that should be part of this check-in list.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% bootstrap_field form.all_products layout="control" %}
|
||||
{% bootstrap_field form.limit_products layout="control" %}
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
125
src/pretix/control/templates/pretixcontrol/checkin/lists.html
Normal file
125
src/pretix/control/templates/pretixcontrol/checkin/lists.html
Normal file
@@ -0,0 +1,125 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Check-in lists" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Check-in lists" %}</h1>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
You can create check-in lists that you can use e.g. at the entrance of your event to track who is coming
|
||||
and if they actually bought a ticket. You can do this process by printing out the list on paper, using this
|
||||
web interface or by using one of our mobile or desktop apps to automatically scan tickets.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
You can create multiple check-in lists to separate multiple parts of your event, for example if you have
|
||||
separate entries for multiple ticket types. Different check-in lists are completely independent: If a ticket
|
||||
shows up on two lists, it is valid once on every list. This might be useful if you run a festival with
|
||||
festival passes that allow access to every or multiple performances as well as tickets only valid for single
|
||||
performances.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% if request.event.has_subevents %}
|
||||
<form class="form-inline helper-display-inline" action="" method="get">
|
||||
<p>
|
||||
{% if request.event.has_subevents %}
|
||||
<select name="subevent" class="form-control">
|
||||
<option value="">{% trans "All dates" context "subevent" %}</option>
|
||||
{% for se in request.event.subevents.all %}
|
||||
<option value="{{ se.id }}"
|
||||
{% if request.GET.subevent|add:0 == se.id %}selected="selected"{% endif %}>
|
||||
{{ se.name }} – {{ se.get_date_range_display }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% endif %}
|
||||
<button class="btn btn-primary" type="submit">{% trans "Filter" %}</button>
|
||||
</p>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if checkinlists|length == 0 %}
|
||||
<div class="empty-collection">
|
||||
<p>
|
||||
{% if request.GET.subevent %}
|
||||
{% trans "Your search did not match any check-in lists." %}
|
||||
{% else %}
|
||||
{% blocktrans trimmed %}
|
||||
You haven't created any check-in lists yet.
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
{% if "can_change_event_settings" in request.eventpermset %}
|
||||
<a href="{% url "control:event.orders.checkinlists.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i>
|
||||
{% trans "Create a new check-in list" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% if "can_change_event_settings" in request.eventpermset %}
|
||||
<p>
|
||||
<a href="{% url "control:event.orders.checkinlists.add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new check-in list" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-quotas">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Check-in lists" %}</th>
|
||||
<th>{% trans "Checked in" %}</th>
|
||||
{% if request.event.has_subevents %}
|
||||
<th>{% trans "Date" context "subevent" %}</th>
|
||||
{% endif %}
|
||||
<th>{% trans "Products" %}</th>
|
||||
<th class="action-col-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for cl in checkinlists %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong><a href="{% url "control:event.orders.checkinlists.show" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}">{{ cl.name }}</a></strong>
|
||||
</td>
|
||||
<td>
|
||||
<div class="quotabox availability">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-success progress-bar-{{ cl.percent }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="numbers">
|
||||
{{ cl.checkin_count|default_if_none:"0" }} / {{ cl.position_count|default_if_none:"0" }}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
{% if request.event.has_subevents %}
|
||||
<td>{{ cl.subevent.name }} – {{ cl.subevent.get_date_range_display }}</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
{% if cl.all_products %}
|
||||
<em>{% trans "All" %}</em>
|
||||
{% else %}
|
||||
<ul>
|
||||
{% for item in cl.limit_products.all %}
|
||||
<li>
|
||||
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.id %}">{{ item.name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<a href="{% url "control:event.orders.checkinlists.show" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}" class="btn btn-default btn-sm"><i class="fa fa-eye"></i></a>
|
||||
{% if "can_change_event_settings" in request.eventpermset %}
|
||||
<a href="{% url "control:event.orders.checkinlists.edit" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
<a href="{% url "control:event.orders.checkinlists.delete" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
{% endblock %}
|
||||
@@ -101,12 +101,6 @@
|
||||
{% trans "Waiting list" %}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'control:event.orders.checkins' organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
{% if url_name == "event.orders.checkins" %}class="active"{% endif %}>
|
||||
{% trans "Check-ins" %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
@@ -119,6 +113,15 @@
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if 'can_view_orders' in request.eventpermset %}
|
||||
<li>
|
||||
<a href="{% url 'control:event.orders.checkinlists' organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
{% if "event.orders.checkin" in url_name %}class="active"{% endif %}>
|
||||
<i class="fa fa-check-square-o fa-fw"></i>
|
||||
{% trans "Check-in lists" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% for nav in nav_event %}
|
||||
<li>
|
||||
<a href="{{ nav.url }}" {% if nav.active %}class="active"{% endif %}
|
||||
|
||||
@@ -187,7 +187,9 @@
|
||||
– {{ line.variation }}
|
||||
{% endif %}
|
||||
{% if line.checkins.all %}
|
||||
<span class="fa fa-check" data-toggle="tooltip" title="{% blocktrans trimmed with date=line.checkins.all.0.datetime|date:'d.m.Y H:i' %}First scanned: {{ date }}{% endblocktrans %}"></span>
|
||||
{% for c in line.checkins.all %}
|
||||
<span class="fa fa-fw fa-check" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'d.m.Y H:i' %}First scanned: {{ date }}{% endblocktrans %}"></span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if line.voucher %}
|
||||
<br/><span class="fa fa-tags"></span> {% trans "Voucher code used:" %}
|
||||
|
||||
@@ -4,23 +4,36 @@
|
||||
{% load order_overview %}
|
||||
{% block title %}{% trans "Data export" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Data export" %}</h1>
|
||||
<h1>
|
||||
{% trans "Data export" %}
|
||||
{% if "identifier" in request.GET %}
|
||||
<a href="?" class="btn btn-default">{% trans "Show all" %}</a>
|
||||
{% endif %}
|
||||
</h1>
|
||||
{% for e in exporters %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{{ e.verbose_name }}</h3>
|
||||
<h3 class="panel-title">
|
||||
<a class="collapsed" data-toggle="collapse" href="#{{ e.identifier }}">
|
||||
{{ e.verbose_name }}
|
||||
<i class="fa fa-angle-down collapse-indicator"></i>
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form action="{% url "control:event.orders.export.do" event=request.event.slug organizer=request.organizer.slug %}"
|
||||
method="post" class="form-horizontal" data-asynctask data-asynctask-download
|
||||
data-asynctask-long>
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="exporter" value="{{ e.identifier }}" />
|
||||
{% bootstrap_form e.form layout='horizontal' %}
|
||||
<button class="btn btn-primary pull-right" type="submit">
|
||||
<span class="icon icon-upload"></span> {% trans "Start export" %}
|
||||
</button>
|
||||
</form>
|
||||
<div id="{{ e.identifier }}" class="panel-collapse collapse {% if "identifier" in request.GET %}in
|
||||
{% endif %}">
|
||||
<div class="panel-body">
|
||||
<form action="{% url "control:event.orders.export.do" event=request.event.slug organizer=request.organizer.slug %}"
|
||||
method="post" class="form-horizontal" data-asynctask data-asynctask-download
|
||||
data-asynctask-long>
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="exporter" value="{{ e.identifier }}" />
|
||||
{% bootstrap_form e.form layout='horizontal' %}
|
||||
<button class="btn btn-primary pull-right" type="submit">
|
||||
<span class="icon icon-upload"></span> {% trans "Start export" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
@@ -123,6 +123,72 @@
|
||||
{% bootstrap_field f.price layout="control" %}
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Check-in lists" %}</legend>
|
||||
<div class="formset" data-formset data-formset-prefix="{{ cl_formset.prefix }}">
|
||||
{{ cl_formset.management_form }}
|
||||
{% bootstrap_formset_errors cl_formset %}
|
||||
<div data-formset-body>
|
||||
{% for form in cl_formset %}
|
||||
<div class="panel panel-default" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ form.id }}
|
||||
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
{% bootstrap_field form.name layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-md-2 text-right">
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="panel-body form-horizontal">
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.all_products layout="control" %}
|
||||
{% bootstrap_field form.limit_products layout="control" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<script type="form-template" data-formset-empty-form>
|
||||
{% escapescript %}
|
||||
<div class="panel panel-default" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ cl_formset.empty_form.id }}
|
||||
{% bootstrap_field cl_formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
{% bootstrap_field cl_formset.empty_form.name layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-md-2 text-right">
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="panel-body form-horizontal">
|
||||
{% bootstrap_field cl_formset.empty_form.all_products layout="control" %}
|
||||
{% bootstrap_field cl_formset.empty_form.limit_products layout="control" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endescapescript %}
|
||||
</script>
|
||||
<p>
|
||||
<button type="button" class="btn btn-default" data-formset-add>
|
||||
<i class="fa fa-plus"></i> {% trans "Add a new check-in list" %}
|
||||
</button>
|
||||
</p>
|
||||
</fieldset>
|
||||
</div>
|
||||
{% if subevent.pk %}
|
||||
<div class="col-xs-12 col-lg-2">
|
||||
|
||||
@@ -164,7 +164,14 @@ urlpatterns = [
|
||||
url(r'^orders/$', orders.OrderList.as_view(), name='event.orders'),
|
||||
url(r'^waitinglist/$', waitinglist.WaitingListView.as_view(), name='event.orders.waitinglist'),
|
||||
url(r'^waitinglist/auto_assign$', waitinglist.AutoAssign.as_view(), name='event.orders.waitinglist.auto'),
|
||||
url(r'^waitinglist/(?P<entry>\d+)/delete$', waitinglist.EntryDelete.as_view(), name='event.orders.waitinglist.delete'),
|
||||
url(r'^checkins/$', checkin.CheckInView.as_view(), name='event.orders.checkins'),
|
||||
url(r'^waitinglist/(?P<entry>\d+)/delete$', waitinglist.EntryDelete.as_view(),
|
||||
name='event.orders.waitinglist.delete'),
|
||||
url(r'^checkinlists/$', checkin.CheckinListList.as_view(), name='event.orders.checkinlists'),
|
||||
url(r'^checkinlists/add$', checkin.CheckinListCreate.as_view(), name='event.orders.checkinlists.add'),
|
||||
url(r'^checkinlists/(?P<list>\d+)/$', checkin.CheckInListShow.as_view(), name='event.orders.checkinlists.show'),
|
||||
url(r'^checkinlists/(?P<list>\d+)/change$', checkin.CheckinListUpdate.as_view(),
|
||||
name='event.orders.checkinlists.edit'),
|
||||
url(r'^checkinlists/(?P<list>\d+)/delete$', checkin.CheckinListDelete.as_view(),
|
||||
name='event.orders.checkinlists.delete'),
|
||||
])),
|
||||
]
|
||||
|
||||
@@ -1,122 +1,221 @@
|
||||
import dateutil.parser
|
||||
from django.contrib import messages
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db.models import F, Prefetch, Q
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.shortcuts import redirect
|
||||
from django.utils.timezone import now
|
||||
from django.db import transaction
|
||||
from django.db.models import Max, OuterRef, Subquery
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.generic import ListView
|
||||
from django.views.generic import DeleteView, ListView
|
||||
from pytz import UTC
|
||||
|
||||
from pretix.base.models import Checkin, Item, Order, OrderPosition
|
||||
from pretix.base.models import Checkin, Order, OrderPosition
|
||||
from pretix.base.models.checkin import CheckinList
|
||||
from pretix.control.forms.checkin import CheckinListForm
|
||||
from pretix.control.forms.filter import CheckInFilterForm
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.control.views import CreateView, UpdateView
|
||||
|
||||
|
||||
class CheckInView(EventPermissionRequiredMixin, ListView):
|
||||
class CheckInListShow(EventPermissionRequiredMixin, ListView):
|
||||
model = Checkin
|
||||
context_object_name = 'entries'
|
||||
paginate_by = 30
|
||||
template_name = 'pretixcontrol/checkin/index.html'
|
||||
permission = 'can_view_orders'
|
||||
|
||||
def get_queryset(self):
|
||||
def get_queryset(self, filter=True):
|
||||
cqs = Checkin.objects.filter(
|
||||
position_id=OuterRef('pk'),
|
||||
list_id=self.list.pk
|
||||
).order_by().values('position_id').annotate(
|
||||
m=Max('datetime')
|
||||
).values('m')
|
||||
|
||||
qs = OrderPosition.objects.filter(order__event=self.request.event, order__status='p')
|
||||
qs = OrderPosition.objects.filter(
|
||||
order__event=self.request.event,
|
||||
order__status=Order.STATUS_PAID,
|
||||
subevent=self.list.subevent
|
||||
).annotate(
|
||||
last_checked_in=Subquery(cqs)
|
||||
).select_related('item', 'variation', 'order', 'addon_to')
|
||||
|
||||
# if this setting is False, we check only items for admission
|
||||
if not self.request.event.settings.ticket_download_nonadm:
|
||||
qs = qs.filter(item__admission=True)
|
||||
if not self.list.all_products:
|
||||
qs = qs.filter(item__in=self.list.limit_products.values_list('id', flat=True))
|
||||
|
||||
if self.request.GET.get("status", "") != "":
|
||||
p = self.request.GET.get("status", "")
|
||||
if p == '1':
|
||||
# records with check-in record
|
||||
qs = qs.filter(checkins__isnull=False)
|
||||
elif p == '0':
|
||||
qs = qs.filter(checkins__isnull=True)
|
||||
if filter and self.filter_form.is_valid():
|
||||
qs = self.filter_form.filter_qs(qs)
|
||||
|
||||
if self.request.GET.get("user", "") != "":
|
||||
u = self.request.GET.get("user", "")
|
||||
qs = qs.filter(
|
||||
Q(order__email__icontains=u) | Q(attendee_name__icontains=u) | Q(attendee_email__icontains=u)
|
||||
)
|
||||
return qs
|
||||
|
||||
if self.request.GET.get("item", "") != "":
|
||||
u = self.request.GET.get("item", "")
|
||||
qs = qs.filter(item_id=u)
|
||||
@cached_property
|
||||
def filter_form(self):
|
||||
return CheckInFilterForm(
|
||||
data=self.request.GET,
|
||||
event=self.request.event,
|
||||
list=self.list
|
||||
)
|
||||
|
||||
if self.request.GET.get("subevent", "") != "":
|
||||
s = self.request.GET.get("subevent", "")
|
||||
qs = qs.filter(subevent_id=s)
|
||||
|
||||
qs = qs.prefetch_related(
|
||||
Prefetch('checkins', queryset=Checkin.objects.filter(position__order__event=self.request.event))
|
||||
).select_related('order', 'item', 'addon_to')
|
||||
|
||||
if self.request.GET.get("ordering", "") != "":
|
||||
p = self.request.GET.get("ordering", "")
|
||||
keys_allowed = self.get_ordering_keys_mappings()
|
||||
if p in keys_allowed:
|
||||
mapped_field = keys_allowed[p]
|
||||
if isinstance(mapped_field, dict):
|
||||
order = mapped_field.pop('_order')
|
||||
qs = qs.annotate(**mapped_field).order_by(order)
|
||||
elif isinstance(mapped_field, (list, tuple)):
|
||||
qs = qs.order_by(*mapped_field)
|
||||
else:
|
||||
qs = qs.order_by(mapped_field)
|
||||
|
||||
return qs.distinct()
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.list = get_object_or_404(self.request.event.checkin_lists.all(), pk=kwargs.get("list"))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['items'] = Item.objects.filter(event=self.request.event)
|
||||
ctx['filtered'] = ("status" in self.request.GET or "user" in self.request.GET or "item" in self.request.GET
|
||||
or "subevent" in self.request.GET)
|
||||
ctx['checkinlist'] = self.list
|
||||
ctx['filter_form'] = self.filter_form
|
||||
for e in ctx['entries']:
|
||||
if e.last_checked_in:
|
||||
if isinstance(e.last_checked_in, str):
|
||||
# Apparently only happens on SQLite
|
||||
e.last_checked_in_aware = make_aware(dateutil.parser.parse(e.last_checked_in), UTC)
|
||||
else:
|
||||
e.last_checked_in_aware = e.last_checked_in
|
||||
return ctx
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
positions = OrderPosition.objects.select_related('item', 'variation', 'order', 'addon_to').filter(
|
||||
order__event=self.request.event,
|
||||
if "can_change_orders" not in request.eventpermset:
|
||||
messages.error(request, _('You do not have permission to perform this action.'))
|
||||
return redirect(reverse('control:event.orders.checkins', kwargs={
|
||||
'event': self.request.event.slug,
|
||||
'organizer': self.request.event.organizer.slug
|
||||
}) + '?' + request.GET.urlencode())
|
||||
|
||||
positions = self.get_queryset(filter=False).filter(
|
||||
pk__in=request.POST.getlist('checkin')
|
||||
)
|
||||
|
||||
for op in positions:
|
||||
created = False
|
||||
if op.order.status == Order.STATUS_PAID:
|
||||
ci, created = Checkin.objects.get_or_create(position=op, defaults={
|
||||
ci, created = Checkin.objects.get_or_create(position=op, list=self.list, defaults={
|
||||
'datetime': now(),
|
||||
})
|
||||
op.order.log_action('pretix.control.views.checkin', data={
|
||||
'position': op.id,
|
||||
'positionid': op.positionid,
|
||||
'first': created,
|
||||
'datetime': now()
|
||||
'datetime': now(),
|
||||
'list': self.list.pk
|
||||
}, user=request.user)
|
||||
|
||||
messages.success(request, _('The selected tickets have been marked as checked in.'))
|
||||
return redirect(reverse('control:event.orders.checkins', kwargs={
|
||||
return redirect(reverse('control:event.orders.checkinlists.show', kwargs={
|
||||
'event': self.request.event.slug,
|
||||
'organizer': self.request.event.organizer.slug
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'list': self.list.pk
|
||||
}) + '?' + request.GET.urlencode())
|
||||
|
||||
@staticmethod
|
||||
def get_ordering_keys_mappings():
|
||||
return {
|
||||
'code': 'order__code',
|
||||
'-code': '-order__code',
|
||||
'email': 'order__email',
|
||||
'-email': '-order__email',
|
||||
# Set nulls_first to be consistent over databases
|
||||
'status': F('checkins__id').asc(nulls_first=True),
|
||||
'-status': F('checkins__id').desc(nulls_last=True),
|
||||
'timestamp': F('checkins__datetime').asc(nulls_first=True),
|
||||
'-timestamp': F('checkins__datetime').desc(nulls_last=True),
|
||||
'item': ('item__name', 'variation__value'),
|
||||
'-item': ('-item__name', 'variation__value'),
|
||||
'subevent': ('subevent__date_from', 'subevent__name'),
|
||||
'-subevent': ('-subevent__date_from', '-subevent__name'),
|
||||
'name': {'_order': F('display_name').asc(nulls_first=True),
|
||||
'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')},
|
||||
'-name': {'_order': F('display_name').desc(nulls_last=True),
|
||||
'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')},
|
||||
}
|
||||
|
||||
class CheckinListList(EventPermissionRequiredMixin, ListView):
|
||||
model = CheckinList
|
||||
context_object_name = 'checkinlists'
|
||||
paginate_by = 30
|
||||
permission = 'can_view_orders'
|
||||
template_name = 'pretixcontrol/checkin/lists.html'
|
||||
|
||||
def get_queryset(self):
|
||||
qs = self.request.event.checkin_lists.prefetch_related("limit_products")
|
||||
qs = CheckinList.annotate_with_numbers(qs, self.request.event)
|
||||
|
||||
if self.request.GET.get("subevent", "") != "":
|
||||
s = self.request.GET.get("subevent", "")
|
||||
qs = qs.filter(subevent_id=s)
|
||||
return qs
|
||||
|
||||
|
||||
class CheckinListCreate(EventPermissionRequiredMixin, CreateView):
|
||||
model = CheckinList
|
||||
form_class = CheckinListForm
|
||||
template_name = 'pretixcontrol/checkin/list_edit.html'
|
||||
permission = 'can_change_event_settings'
|
||||
context_object_name = 'checkinlist'
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.orders.checkinlists', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
})
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
form.instance.event = self.request.event
|
||||
messages.success(self.request, _('The new check-in list has been created.'))
|
||||
ret = super().form_valid(form)
|
||||
form.instance.log_action('pretix.event.checkinlist.added', user=self.request.user,
|
||||
data=dict(form.cleaned_data))
|
||||
return ret
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.error(self.request, _('We could not save your changes. See below for details.'))
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class CheckinListUpdate(EventPermissionRequiredMixin, UpdateView):
|
||||
model = CheckinList
|
||||
form_class = CheckinListForm
|
||||
template_name = 'pretixcontrol/checkin/list_edit.html'
|
||||
permission = 'can_change_event_settings'
|
||||
context_object_name = 'checkinlist'
|
||||
|
||||
def get_object(self, queryset=None) -> CheckinList:
|
||||
try:
|
||||
return self.request.event.checkin_lists.get(
|
||||
id=self.kwargs['list']
|
||||
)
|
||||
except CheckinList.DoesNotExist:
|
||||
raise Http404(_("The requested list does not exist."))
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
if form.has_changed():
|
||||
self.object.log_action(
|
||||
'pretix.event.checkinlist.changed', user=self.request.user, data={
|
||||
k: form.cleaned_data.get(k) for k in form.changed_data
|
||||
}
|
||||
)
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.orders.checkinlists.show', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
'list': self.object.pk
|
||||
})
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.error(self.request, _('We could not save your changes. See below for details.'))
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class CheckinListDelete(EventPermissionRequiredMixin, DeleteView):
|
||||
model = CheckinList
|
||||
template_name = 'pretixcontrol/checkin/list_delete.html'
|
||||
permission = 'can_change_event_settings'
|
||||
context_object_name = 'checkinlist'
|
||||
|
||||
def get_object(self, queryset=None) -> CheckinList:
|
||||
try:
|
||||
return self.request.event.checkin_lists.get(
|
||||
id=self.kwargs['list']
|
||||
)
|
||||
except CheckinList.DoesNotExist:
|
||||
raise Http404(_("The requested list does not exist."))
|
||||
|
||||
@transaction.atomic
|
||||
def delete(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
success_url = self.get_success_url()
|
||||
self.object.log_action(action='pretix.event.orders.deleted', user=request.user)
|
||||
self.object.delete()
|
||||
messages.success(self.request, _('The selected list has been deleted.'))
|
||||
return HttpResponseRedirect(success_url)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.orders.checkinlists', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
})
|
||||
|
||||
@@ -20,6 +20,7 @@ from pretix.base.models import (
|
||||
Event, Item, Order, OrderPosition, RequiredAction, SubEvent, Voucher,
|
||||
WaitingListEntry,
|
||||
)
|
||||
from pretix.base.models.checkin import CheckinList
|
||||
from pretix.control.forms.event import CommentForm
|
||||
from pretix.control.signals import (
|
||||
event_dashboard_widgets, user_dashboard_widgets,
|
||||
@@ -190,7 +191,7 @@ def shop_state_widget(sender, **kwargs):
|
||||
|
||||
|
||||
@receiver(signal=event_dashboard_widgets)
|
||||
def checkin_widget(sender, **kwargs):
|
||||
def checkin_widget(sender, subevent=None, **kwargs):
|
||||
size_qs = OrderPosition.objects.filter(order__event=sender, order__status='p')
|
||||
checked_qs = OrderPosition.objects.filter(order__event=sender, order__status='p', checkins__isnull=False)
|
||||
|
||||
@@ -199,15 +200,22 @@ def checkin_widget(sender, **kwargs):
|
||||
size_qs = size_qs.filter(item__admission=True)
|
||||
checked_qs = checked_qs.filter(item__admission=True)
|
||||
|
||||
return [{
|
||||
'content': NUM_WIDGET.format(num='{}/{}'.format(checked_qs.count(), size_qs.count()), text=_('Checked in')),
|
||||
'display_size': 'small',
|
||||
'priority': 50,
|
||||
'url': reverse('control:event.orders.checkins', kwargs={
|
||||
'event': sender.slug,
|
||||
'organizer': sender.organizer.slug
|
||||
widgets = []
|
||||
qs = sender.checkin_lists.filter(subevent=subevent)
|
||||
qs = CheckinList.annotate_with_numbers(qs, sender)
|
||||
for cl in qs:
|
||||
widgets.append({
|
||||
'content': NUM_WIDGET.format(num='{}/{}'.format(cl.checkin_count, cl.position_count),
|
||||
text=_('Checked in – {list}').format(list=escape(cl.name))),
|
||||
'display_size': 'small',
|
||||
'priority': 50,
|
||||
'url': reverse('control:event.orders.checkinlists.show', kwargs={
|
||||
'event': sender.slug,
|
||||
'organizer': sender.organizer.slug,
|
||||
'list': cl.pk
|
||||
})
|
||||
})
|
||||
}]
|
||||
return widgets
|
||||
|
||||
|
||||
@receiver(signal=event_dashboard_widgets)
|
||||
|
||||
@@ -15,6 +15,7 @@ from django.views.generic import ListView
|
||||
from formtools.wizard.views import SessionWizardView
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import Event, Organizer, Quota, Team
|
||||
from pretix.control.forms.event import (
|
||||
EventWizardBasicsForm, EventWizardCopyForm, EventWizardFoundationForm,
|
||||
@@ -143,7 +144,7 @@ class EventWizard(SessionWizardView):
|
||||
basics_data = self.get_cleaned_data_for_step('basics')
|
||||
copy_data = self.get_cleaned_data_for_step('copy')
|
||||
|
||||
with transaction.atomic():
|
||||
with transaction.atomic(), language(basics_data['locale']):
|
||||
event = form_dict['basics'].instance
|
||||
event.organizer = foundation_data['organizer']
|
||||
event.plugins = settings.PRETIX_PLUGINS_DEFAULT
|
||||
@@ -165,7 +166,7 @@ class EventWizard(SessionWizardView):
|
||||
t.limit_events.add(event)
|
||||
|
||||
if event.has_subevents:
|
||||
event.subevents.create(
|
||||
se = event.subevents.create(
|
||||
name=event.name,
|
||||
date_from=event.date_from,
|
||||
date_to=event.date_to,
|
||||
@@ -191,6 +192,17 @@ class EventWizard(SessionWizardView):
|
||||
if copy_data and copy_data['copy_from_event']:
|
||||
from_event = copy_data['copy_from_event']
|
||||
event.copy_data_from(from_event)
|
||||
elif event.has_subevents:
|
||||
event.checkin_lists.create(
|
||||
name=str(se),
|
||||
all_products=True,
|
||||
subevent=se
|
||||
)
|
||||
else:
|
||||
event.checkin_lists.create(
|
||||
name=_('Default'),
|
||||
all_products=True
|
||||
)
|
||||
|
||||
event.settings.set('timezone', basics_data['timezone'])
|
||||
event.settings.set('locale', basics_data['locale'])
|
||||
|
||||
@@ -147,7 +147,7 @@ class OrderDetail(OrderView):
|
||||
).select_related(
|
||||
'item', 'variation', 'addon_to', 'tax_rule'
|
||||
).prefetch_related(
|
||||
'item__questions', 'answers', 'answers__question', 'checkins'
|
||||
'item__questions', 'answers', 'answers__question', 'checkins', 'checkins__list'
|
||||
).order_by('positionid')
|
||||
|
||||
positions = []
|
||||
@@ -906,9 +906,21 @@ class ExportMixin:
|
||||
responses = register_data_exporters.send(self.request.event)
|
||||
for receiver, response in responses:
|
||||
ex = response(self.request.event)
|
||||
if self.request.GET.get("identifier") and ex.identifier != self.request.GET.get("identifier"):
|
||||
continue
|
||||
|
||||
# Use form parse cycle to generate useful defaults
|
||||
test_form = ExporterForm(data=self.request.GET, prefix=ex.identifier)
|
||||
test_form.fields = ex.export_form_fields
|
||||
test_form.is_valid()
|
||||
initial = {
|
||||
k: v for k, v in test_form.cleaned_data.items() if ex.identifier + "-" + k in self.request.GET
|
||||
}
|
||||
|
||||
ex.form = ExporterForm(
|
||||
data=(self.request.POST if self.request.method == 'POST' else None),
|
||||
prefix=ex.identifier
|
||||
prefix=ex.identifier,
|
||||
initial=initial
|
||||
)
|
||||
ex.form.fields = ex.export_form_fields
|
||||
exporters.append(ex)
|
||||
|
||||
@@ -11,13 +11,15 @@ from django.utils.functional import cached_property
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from django.views.generic import CreateView, DeleteView, ListView, UpdateView
|
||||
|
||||
from pretix.base.models.checkin import CheckinList
|
||||
from pretix.base.models.event import SubEvent, SubEventMetaValue
|
||||
from pretix.base.models.items import Quota, SubEventItem, SubEventItemVariation
|
||||
from pretix.control.forms.checkin import CheckinListForm
|
||||
from pretix.control.forms.filter import SubEventFilterForm
|
||||
from pretix.control.forms.item import QuotaForm
|
||||
from pretix.control.forms.subevents import (
|
||||
QuotaFormSet, SubEventForm, SubEventItemForm, SubEventItemVariationForm,
|
||||
SubEventMetaValueForm,
|
||||
CheckinListFormSet, QuotaFormSet, SubEventForm, SubEventItemForm,
|
||||
SubEventItemVariationForm, SubEventMetaValueForm,
|
||||
)
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.control.views.event import MetaDataEditorMixin
|
||||
@@ -132,6 +134,41 @@ class SubEventEditorMixin(MetaDataEditorMixin):
|
||||
data=(self.request.POST if self.request.method == "POST" else None)
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def cl_formset(self):
|
||||
extra = 0
|
||||
kwargs = {}
|
||||
|
||||
if self.copy_from:
|
||||
kwargs['initial'] = [
|
||||
{
|
||||
'name': cl.name,
|
||||
'all_products': cl.all_products,
|
||||
'limit_products': cl.limit_products.all(),
|
||||
} for cl in self.copy_from.checkinlist_set.prefetch_related('limit_products')
|
||||
]
|
||||
extra = len(kwargs['initial'])
|
||||
elif not self.object:
|
||||
kwargs['initial'] = [
|
||||
{
|
||||
'name': '',
|
||||
'all_products': True,
|
||||
}
|
||||
]
|
||||
extra = 1
|
||||
|
||||
formsetclass = inlineformset_factory(
|
||||
SubEvent, CheckinList,
|
||||
form=CheckinListForm, formset=CheckinListFormSet,
|
||||
can_order=False, can_delete=True, extra=extra,
|
||||
)
|
||||
if self.object:
|
||||
kwargs['queryset'] = self.object.checkinlist_set.prefetch_related('limit_products')
|
||||
|
||||
return formsetclass(self.request.POST if self.request.method == "POST" else None,
|
||||
instance=self.object,
|
||||
event=self.request.event, **kwargs)
|
||||
|
||||
@cached_property
|
||||
def formset(self):
|
||||
extra = 0
|
||||
@@ -161,6 +198,38 @@ class SubEventEditorMixin(MetaDataEditorMixin):
|
||||
instance=self.object,
|
||||
event=self.request.event, **kwargs)
|
||||
|
||||
def save_cl_formset(self, obj):
|
||||
for form in self.cl_formset.initial_forms:
|
||||
if form in self.cl_formset.deleted_forms:
|
||||
if not form.instance.pk:
|
||||
continue
|
||||
form.instance.log_action(action='pretix.event.checkinlist.deleted', user=self.request.user)
|
||||
form.instance.delete()
|
||||
form.instance.pk = None
|
||||
elif form.has_changed():
|
||||
form.instance.subevent = obj
|
||||
form.instance.event = obj.event
|
||||
form.save()
|
||||
change_data = {k: form.cleaned_data.get(k) for k in form.changed_data}
|
||||
change_data['id'] = form.instance.pk
|
||||
form.instance.log_action(
|
||||
'pretix.event.checkinlist.changed', user=self.request.user, data={
|
||||
k: form.cleaned_data.get(k) for k in form.changed_data
|
||||
}
|
||||
)
|
||||
|
||||
for form in self.cl_formset.extra_forms:
|
||||
if not form.has_changed():
|
||||
continue
|
||||
if self.formset._should_delete_form(form):
|
||||
continue
|
||||
form.instance.subevent = obj
|
||||
form.instance.event = obj.event
|
||||
form.save()
|
||||
change_data = {k: form.cleaned_data.get(k) for k in form.changed_data}
|
||||
change_data['id'] = form.instance.pk
|
||||
form.instance.log_action(action='pretix.event.checkinlist.added', user=self.request.user, data=change_data)
|
||||
|
||||
def save_formset(self, obj):
|
||||
for form in self.formset.initial_forms:
|
||||
if form in self.formset.deleted_forms:
|
||||
@@ -204,6 +273,7 @@ class SubEventEditorMixin(MetaDataEditorMixin):
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['formset'] = self.formset
|
||||
ctx['cl_formset'] = self.cl_formset
|
||||
ctx['itemvar_forms'] = self.itemvar_forms
|
||||
ctx['meta_forms'] = self.meta_forms
|
||||
return ctx
|
||||
@@ -259,7 +329,7 @@ class SubEventEditorMixin(MetaDataEditorMixin):
|
||||
def is_valid(self, form):
|
||||
return form.is_valid() and all([f.is_valid() for f in self.itemvar_forms]) and self.formset.is_valid() and (
|
||||
all([f.is_valid() for f in self.meta_forms])
|
||||
)
|
||||
) and self.cl_formset.is_valid()
|
||||
|
||||
|
||||
class SubEventUpdate(EventPermissionRequiredMixin, SubEventEditorMixin, UpdateView):
|
||||
@@ -288,6 +358,7 @@ class SubEventUpdate(EventPermissionRequiredMixin, SubEventEditorMixin, UpdateVi
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
self.save_formset(self.object)
|
||||
self.save_cl_formset(self.object)
|
||||
self.save_meta()
|
||||
|
||||
for f in self.itemvar_forms:
|
||||
@@ -355,6 +426,7 @@ class SubEventCreate(SubEventEditorMixin, EventPermissionRequiredMixin, CreateVi
|
||||
form.instance.log_action('pretix.subevent.added', data=dict(form.cleaned_data), user=self.request.user)
|
||||
|
||||
self.save_formset(form.instance)
|
||||
self.save_cl_formset(form.instance)
|
||||
for f in self.itemvar_forms:
|
||||
f.instance.subevent = form.instance
|
||||
f.save()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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" %}
|
||||
|
||||
@@ -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'] = []
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -261,7 +261,8 @@ REST_FRAMEWORK = {
|
||||
CORE_MODULES = {
|
||||
"pretix.base",
|
||||
"pretix.presale",
|
||||
"pretix.control"
|
||||
"pretix.control",
|
||||
"pretix.plugins.checkinlists",
|
||||
}
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
||||
@@ -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"));
|
||||
});
|
||||
|
||||
16
src/pretix/static/pretixcontrol/js/ui/subevent.js
Normal file
16
src/pretix/static/pretixcontrol/js/ui/subevent.js
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
168
src/tests/api/test_checkin.py
Normal file
168
src/tests/api/test_checkin.py
Normal 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"
|
||||
@@ -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))
|
||||
|
||||
@@ -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),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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"))
|
||||
|
||||
Reference in New Issue
Block a user