Fix #978 -- Allow to split names (#1049)

- [x] attendee names
- [x] Invoice address names
- [x] Data migration
- [x] API serializers
  - [x] orderposition
  - [x] cartposition
  - [x] invoiceaddress
  - [x] checkinlistposition
- [x] position API search
- [x] invoice API search
- [x] business/individual required toggle
- [x] Split columns in CSV exports
- [x] ticket editor
- [x] shredder
- [x] ticket/invoice sample data
- [x] order search
- [x] Handle changed naming scheme
- [x] tests
- [x] make use in:
  - [x] Boabee
  - [x] Certificate download order
  - [x] Badge download order
  - [x] Ticket download order
- [x] Document new MySQL requirement
- [x] Plugins
This commit is contained in:
Raphael Michel
2018-11-05 15:43:21 +01:00
committed by GitHub
parent 7039374588
commit 94be46ffdb
71 changed files with 1219 additions and 244 deletions

View File

@@ -4,11 +4,14 @@ from io import BytesIO
from typing import Tuple
from django import forms
from django.conf import settings
from django.contrib.staticfiles import finders
from django.core.files import File
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage
from django.db.models.functions import Coalesce
from django.utils.translation import ugettext as _
from jsonfallback.functions import JSONExtract
from PyPDF2 import PdfFileMerger
from reportlab.lib import pagesizes
from reportlab.pdfgen import canvas
@@ -17,6 +20,7 @@ from pretix.base.exporter import BaseExporter
from pretix.base.i18n import language
from pretix.base.models import Order, OrderPosition
from pretix.base.pdf import Renderer
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.plugins.badges.models import BadgeItem, BadgeLayout
@@ -67,6 +71,7 @@ class BadgeExporter(BaseExporter):
@property
def export_form_fields(self):
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
d = OrderedDict(
[
('items',
@@ -86,10 +91,13 @@ class BadgeExporter(BaseExporter):
('order_by',
forms.ChoiceField(
label=_('Sort by'),
choices=(
choices=[
('name', _('Attendee name')),
('last_name', _('Last part of attendee name')),
)
('code', _('Order code')),
] + ([
('name:{}'.format(k), _('Attendee name: {part}').format(part=label))
for k, label, w in name_scheme['fields']
] if settings.JSON_FIELD_AVAILABLE and len(name_scheme['fields']) > 1 else []),
)),
]
)
@@ -108,10 +116,18 @@ class BadgeExporter(BaseExporter):
qs = qs.filter(order__status__in=[Order.STATUS_PAID])
if form_data.get('order_by') == 'name':
qs = qs.order_by('attendee_name', 'order__code')
elif form_data.get('order_by') == 'last_name':
qs = qs.order_by('attendee_name_cached', 'order__code')
elif form_data.get('order_by') == 'code':
qs = qs.order_by('order__code')
qs = sorted(qs, key=lambda op: op.attendee_name.split()[-1] if op.attendee_name else '')
elif form_data.get('order_by', '').startswith('name:'):
part = form_data['order_by'][5:]
qs = qs.annotate(
resolved_name=Coalesce('attendee_name_parts', 'addon_to__attendee_name_parts', 'order__invoice_address__name_parts')
).annotate(
resolved_name_part=JSONExtract('resolved_name', part)
).order_by(
'resolved_name_part'
)
outbuffer = render_pdf(self.event, qs)
return 'badges.pdf', 'application/pdf', outbuffer.read()

View File

@@ -170,7 +170,7 @@ class ActionView(View):
qs = self.order_qs().order_by('pk').annotate(inr=Concat('invoices__prefix', 'invoices__invoice_no')).filter(
code
| Q(email__icontains=u)
| Q(positions__attendee_name__icontains=u)
| Q(positions__attendee_name_cached__icontains=u)
| Q(positions__attendee_email__icontains=u)
| Q(invoice_address__name__icontains=u)
| Q(invoice_address__company__icontains=u)

View File

@@ -4,18 +4,21 @@ from collections import OrderedDict
import dateutil.parser
from defusedcsv import csv
from django import forms
from django.conf import settings
from django.db.models import Max, OuterRef, Subquery
from django.db.models.functions import Coalesce
from django.urls import reverse
from django.utils.formats import date_format
from django.utils.timezone import is_aware, make_aware
from django.utils.translation import pgettext, ugettext as _, ugettext_lazy
from jsonfallback.functions import JSONExtract
from pytz import UTC
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 Checkin, Order, OrderPosition, Question
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.templatetags.money import money_filter
from pretix.control.forms.widgets import Select2
from pretix.plugins.reports.exporters import ReportlabExportMixin
@@ -24,6 +27,7 @@ from pretix.plugins.reports.exporters import ReportlabExportMixin
class BaseCheckinList(BaseExporter):
@property
def export_form_fields(self):
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
d = OrderedDict(
[
('list',
@@ -44,10 +48,13 @@ class BaseCheckinList(BaseExporter):
forms.ChoiceField(
label=_('Sort by'),
initial='name',
choices=(
choices=[
('name', _('Attendee name')),
('code', _('Order code')),
),
] + ([
('name:{}'.format(k), _('Attendee name: {part}').format(part=label))
for k, label, w in name_scheme['fields']
] if settings.JSON_FIELD_AVAILABLE and len(name_scheme['fields']) > 1 else []),
widget=forms.RadioSelect,
required=False
)),
@@ -79,6 +86,49 @@ class BaseCheckinList(BaseExporter):
return d
def _get_queryset(self, cl, form_data):
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)
).prefetch_related(
'answers', 'answers__question', 'addon_to__answers', 'addon_to__answers__question'
).select_related('order', 'item', 'variation', 'addon_to', 'order__invoice_address')
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_cached', 'addon_to__attendee_name_cached', 'order__invoice_address__name_cached'),
'order__code')
elif form_data['sort'] == 'code':
qs = qs.order_by('order__code')
elif form_data['sort'].startswith('name:'):
part = form_data['sort'][5:]
qs = qs.annotate(
resolved_name=Coalesce('attendee_name_parts', 'addon_to__attendee_name_parts', 'order__invoice_address__name_parts')
).annotate(
resolved_name_part=JSONExtract('resolved_name', part)
).order_by(
'resolved_name_part'
)
if not cl.include_pending:
qs = qs.filter(order__status=Order.STATUS_PAID)
else:
qs = qs.filter(order__status__in=(Order.STATUS_PAID, Order.STATUS_PENDING))
return qs
class CBFlowable(Flowable):
def __init__(self, checked=False):
@@ -174,37 +224,7 @@ class PDFCheckinList(ReportlabExportMixin, BaseCheckinList):
p = Paragraph(txt, headrowstyle)
tdata[0].append(p)
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', 'addon_to__answers', 'addon_to__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 not cl.include_pending:
qs = qs.filter(order__status=Order.STATUS_PAID)
else:
qs = qs.filter(order__status__in=(Order.STATUS_PAID, Order.STATUS_PENDING))
qs = self._get_queryset(cl, form_data)
for op in qs:
try:
@@ -216,7 +236,7 @@ class PDFCheckinList(ReportlabExportMixin, BaseCheckinList):
name = op.attendee_name or (op.addon_to.attendee_name if op.addon_to else '') or ian
if iac:
name += "\n" + iac
name += "<br/>" + iac
row = [
'!!' if op.item.checkin_attention else '',
@@ -282,33 +302,18 @@ class CSVCheckinList(BaseCheckinList):
questions = list(Question.objects.filter(event=self.event, id__in=form_data['questions']))
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)
).prefetch_related(
'answers', 'answers__question', 'addon_to__answers', 'addon_to__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':
qs = qs.order_by('order__code')
qs = self._get_queryset(cl, form_data)
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
headers = [
_('Order code'), _('Attendee name'), _('Product'), _('Price'), _('Checked in')
_('Order code'),
_('Attendee name'),
]
if len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
headers.append(_('Attendee name: {part}').format(part=label))
headers += [
_('Product'), _('Price'), _('Checked in')
]
if not cl.include_pending:
qs = qs.filter(order__status=Order.STATUS_PAID)
@@ -340,6 +345,13 @@ class CSVCheckinList(BaseCheckinList):
row = [
op.order.code,
op.attendee_name or (op.addon_to.attendee_name if op.addon_to else ''),
]
if len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
row.append(
(op.attendee_name_parts or (op.addon_to.attendee_name_parts if op.addon_to else {})).get(k, '')
)
row += [
str(op.item) + (" " + str(op.variation.value) if op.variation else ""),
op.price,
date_format(last_checked_in.astimezone(self.event.timezone), 'SHORT_DATETIME_FORMAT')

View File

@@ -302,10 +302,10 @@ class ApiSearchView(ApiView):
else:
ops = qs.filter(
Q(secret__istartswith=query)
| Q(attendee_name__icontains=query)
| Q(addon_to__attendee_name__icontains=query)
| Q(attendee_name_cached__icontains=query)
| Q(addon_to__attendee_name_cached__icontains=query)
| Q(order__code__istartswith=query)
| Q(order__invoice_address__name__icontains=query)
| Q(order__invoice_address__name_cached__icontains=query)
)[:25]
response['results'] = [serialize_op(op, bool(op.last_checked_in), self.config.list) for op in ops]

View File

@@ -1,28 +1,80 @@
from collections import OrderedDict
from io import BytesIO
from django import forms
from django.conf import settings
from django.core.files.base import ContentFile
from django.db.models.functions import Coalesce
from django.utils.translation import ugettext as _
from jsonfallback.functions import JSONExtract
from PyPDF2.merger import PdfFileMerger
from pretix.base.exporter import BaseExporter
from pretix.base.i18n import language
from pretix.base.models import Order, OrderPosition
from pretix.base.settings import PERSON_NAME_SCHEMES
from .ticketoutput import PdfTicketOutput
class AllTicketsPDF(BaseExporter):
name = "alltickets"
verbose_name = _("All paid PDF tickets in one file")
verbose_name = _("All PDF tickets in one file")
identifier = "pdfoutput_all_tickets"
@property
def export_form_fields(self):
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
d = OrderedDict(
[
('include_pending',
forms.BooleanField(
label=_('Include pending orders'),
required=False
)),
('order_by',
forms.ChoiceField(
label=_('Sort by'),
choices=[
('name', _('Attendee name')),
('code', _('Order code')),
] + ([
('name:{}'.format(k), _('Attendee name: {part}').format(part=label))
for k, label, w in name_scheme['fields']
] if settings.JSON_FIELD_AVAILABLE and len(name_scheme['fields']) > 1 else []),
)),
]
)
return d
def render(self, form_data):
merger = PdfFileMerger()
o = PdfTicketOutput(self.event)
qs = OrderPosition.objects.filter(order__event=self.event, order__status=Order.STATUS_PAID).select_related(
'order', 'item', 'variation'
)
qs = OrderPosition.objects.filter(
order__event=self.event
).prefetch_related(
'answers', 'answers__question'
).select_related('order', 'item', 'variation', 'addon_to')
if form_data.get('include_pending'):
qs = qs.filter(order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING])
else:
qs = qs.filter(order__status__in=[Order.STATUS_PAID])
if form_data.get('order_by') == 'name':
qs = qs.order_by('attendee_name_cached', 'order__code')
elif form_data.get('order_by') == 'code':
qs = qs.order_by('order__code')
elif form_data.get('order_by', '').startswith('name:'):
part = form_data['order_by'][5:]
qs = qs.annotate(
resolved_name=Coalesce('attendee_name_parts', 'addon_to__attendee_name_parts', 'order__invoice_address__name_parts')
).annotate(
resolved_name_part=JSONExtract('resolved_name', part)
).order_by(
'resolved_name_part'
)
for op in qs:
if op.addon_to_id and not self.event.settings.ticket_download_addons:

View File

@@ -0,0 +1,18 @@
# Generated by Django 2.1 on 2018-10-17 00:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ticketoutputpdf', '0005_merge_20180805_1436'),
]
operations = [
migrations.AlterField(
model_name='ticketlayout',
name='layout',
field=models.TextField(default='[{"italic": false, "bottom": "274.60", "align": "left", "fontfamily": "Open Sans", "width": "175.00", "left": "17.50", "text": "Sample event name", "content": "event_name", "fontsize": "16.0", "bold": false, "color": [0, 0, 0, 1], "type": "textarea"}, {"italic": false, "bottom": "262.90", "align": "left", "fontfamily": "Open Sans", "width": "110.00", "left": "17.50", "text": "Sample product \\u2013 sample variation", "content": "itemvar", "fontsize": "13.0", "bold": false, "color": [0, 0, 0, 1], "type": "textarea"}, {"italic": false, "bottom": "252.50", "align": "left", "fontfamily": "Open Sans", "width": "110.00", "left": "17.50", "text": "John Doe", "content": "attendee_name", "fontsize": "13.0", "bold": false, "color": [0, 0, 0, 1], "type": "textarea"}, {"italic": false, "bottom": "242.10", "align": "left", "fontfamily": "Open Sans", "width": "110.00", "left": "17.50", "text": "May 31st, 2017", "content": "event_date_range", "fontsize": "13.0", "bold": false, "color": [0, 0, 0, 1], "type": "textarea"}, {"italic": false, "bottom": "204.80", "align": "left", "fontfamily": "Open Sans", "width": "110.00", "left": "17.50", "text": "Random City", "content": "event_location", "fontsize": "13.0", "bold": false, "color": [0, 0, 0, 1], "type": "textarea"}, {"italic": false, "bottom": "194.50", "align": "left", "fontfamily": "Open Sans", "width": "30.00", "left": "17.50", "text": "A1B2C", "content": "order", "fontsize": "13.0", "bold": false, "color": [0, 0, 0, 1], "type": "textarea"}, {"italic": false, "bottom": "194.50", "align": "right", "fontfamily": "Open Sans", "width": "45.00", "left": "52.50", "text": "123.45 EUR", "content": "price", "fontsize": "13.0", "bold": false, "color": [0, 0, 0, 1], "type": "textarea"}, {"italic": false, "bottom": "194.50", "align": "left", "fontfamily": "Open Sans", "width": "90.00", "left": "102.50", "text": "tdmruoekvkpbv1o2mv8xccvqcikvr58u", "content": "secret", "fontsize": "13.0", "bold": false, "color": [0, 0, 0, 1], "type": "textarea"}, {"left": "130.40", "bottom": "204.50", "type": "barcodearea", "size": "64.00"},{"type":"poweredby","left":"88.72","bottom":"10.00","size":"20.00","content":"dark"}]'),
),
]