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

* Data model and migration

* Some backwards compatibility

* CRUD for checkin lists

* Show and perform checkins

* Correct numbers in table and dashboard widget

* event creation and cloning

* Allow to link specific exports and pass options per query

* Play with the CSV export

* PDF export

* Collapse exports by default

* Improve PDF exporter

* Addon stuff

* Subevent stuff, pretixdroid tests

* pretixdroid tests

* Add CRUD API

* Test compatibility

* Fix test

* DB-independent sorting behavior

* Add CRUD and coyp tests

* Re-enable pretixdroid plugin

* pretixdroid config

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ from django.dispatch import receiver
from django.utils.formats import date_format
from django.utils.translation import ugettext_lazy as _
from pretix.base.models import CheckinList
from pretix.base.signals import logentry_display
from pretix.control.signals import nav_event
@@ -43,25 +44,40 @@ def pretixcontrol_logentry_display(sender, logentry, **kwargs):
tz = pytz.timezone(sender.settings.timezone)
dt_formatted = date_format(dt.astimezone(tz), "SHORT_DATETIME_FORMAT")
if 'list' in data:
try:
checkin_list = sender.checkin_lists.get(pk=data.get('list')).name
except CheckinList.DoesNotExist:
checkin_list = _("(unknown)")
else:
checkin_list = _("(unknown)")
if data.get('first'):
if show_dt:
return _('Position #{posid} has been scanned at {datetime}.').format(
return _('Position #{posid} has been scanned at {datetime} for list "{list}".').format(
posid=data.get('positionid'),
datetime=dt_formatted
datetime=dt_formatted,
list=checkin_list
)
else:
return _('Position #{posid} has been scanned.').format(
posid=data.get('positionid')
return _('Position #{posid} has been scanned for list "{list}".').format(
posid=data.get('positionid'),
list=checkin_list
)
else:
if data.get('forced'):
return _(
'A scan for position #{posid} at {datetime} has been uploaded even though it has '
'A scan for position #{posid} at {datetime} for list "{list}" has been uploaded even though it has '
'been scanned already.'.format(
posid=data.get('positionid'),
datetime=dt_formatted
datetime=dt_formatted,
list=checkin_list
)
)
return _('Position #{posid} has been scanned and rejected because it has already been scanned before.'.format(
posid=data.get('positionid')
))
return _(
'Position #{posid} has been scanned and rejected because it has already been scanned before '
'on list "{list}".'.format(
posid=data.get('positionid'),
list=checkin_list
)
)

View File

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

View File

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

View File

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