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