mirror of
https://github.com/pretix/pretix.git
synced 2026-05-05 15:14:04 +00:00
* Data model and migration * Some backwards compatibility * CRUD for checkin lists * Show and perform checkins * Correct numbers in table and dashboard widget * event creation and cloning * Allow to link specific exports and pass options per query * Play with the CSV export * PDF export * Collapse exports by default * Improve PDF exporter * Addon stuff * Subevent stuff, pretixdroid tests * pretixdroid tests * Add CRUD API * Test compatibility * Fix test * DB-independent sorting behavior * Add CRUD and coyp tests * Re-enable pretixdroid plugin * pretixdroid config * Tests & fixes
This commit is contained in:
@@ -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'] = []
|
||||
|
||||
Reference in New Issue
Block a user