diff --git a/.travis.sh b/.travis.sh
index 15691b5395..dfdfbd91bb 100755
--- a/.travis.sh
+++ b/.travis.sh
@@ -11,7 +11,6 @@ fi
if [ "$PRETIX_CONFIG_FILE" == "tests/travis_postgres.cfg" ]; then
psql -c 'create database travis_ci_test;' -U postgres
- pip3 install -Ur src/requirements/postgres.txt
fi
if [ "$1" == "style" ]; then
diff --git a/.travis.yml b/.travis.yml
index 8648c01ac8..bfc8ecc99f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -32,6 +32,7 @@ matrix:
env: JOB=translation-spelling
addons:
postgresql: "9.4"
+ mariadb: '10.3'
apt:
packages:
- enchant
diff --git a/Dockerfile b/Dockerfile
index 1adaa53413..05679e80b4 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -30,7 +30,7 @@ RUN chmod +x /usr/local/bin/pretix && \
pip3 install -U pip wheel setuptools && \
cd /pretix/src && \
rm -f pretix.cfg && \
- pip3 install -r requirements.txt -r requirements/mysql.txt -r requirements/postgres.txt \
+ pip3 install -r requirements.txt -r requirements/mysql.txt \
-r requirements/memcached.txt -r requirements/redis.txt gunicorn && \
mkdir -p data && \
chown -R pretixuser:pretixuser /pretix /data data && \
diff --git a/doc/admin/installation/docker_smallscale.rst b/doc/admin/installation/docker_smallscale.rst
index 6a207eb0a8..cada3fb78b 100644
--- a/doc/admin/installation/docker_smallscale.rst
+++ b/doc/admin/installation/docker_smallscale.rst
@@ -26,7 +26,7 @@ installation guides):
* `Docker`_
* A SMTP server to send out mails, e.g. `Postfix`_ on your machine or some third-party server you have credentials for
* A HTTP reverse proxy, e.g. `nginx`_ or Apache to allow HTTPS connections
-* A `MySQL`_ or `PostgreSQL`_ database server
+* A `PostgreSQL`_, `MySQL`_ 5.7+, or MariaDB 10.2.7+ database server
* A `redis`_ server
We also recommend that you use a firewall, although this is not a pretix-specific recommendation. If you're new to
@@ -36,6 +36,9 @@ Linux and firewalls, we recommend that you start with `ufw`_.
SSL certificates can be obtained for free these days. We also *do not* provide support for HTTP-only
installations except for evaluation purposes.
+.. warning:: We recommend **PostgreSQL**. If you go for MySQL, make sure you run **MySQL 5.7 or newer** or
+ **MariaDB 10.2.7 or newer**.
+
On this guide
-------------
@@ -58,7 +61,7 @@ Next, we need a database and a database user. We can create these with any kind
our database's shell, e.g. for MySQL::
$ mysql -u root -p
- mysql> CREATE DATABASE pretix DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;
+ mysql> CREATE DATABASE pretix DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
mysql> GRANT ALL PRIVILEGES ON pretix.* TO pretix@'localhost' IDENTIFIED BY '*********';
mysql> FLUSH PRIVILEGES;
diff --git a/doc/admin/installation/general.rst b/doc/admin/installation/general.rst
index e16d4ad0b2..f15284c202 100644
--- a/doc/admin/installation/general.rst
+++ b/doc/admin/installation/general.rst
@@ -21,6 +21,9 @@ To use pretix, you will need the following things:
.. warning:: Do not ever use SQLite in production. It will break.
+ .. warning:: We recommend **PostgreSQL**. If you go for MySQL, make sure you run **MySQL 5.7 or newer** or
+ **MariaDB 10.2.7 or newer**.
+
* A **reverse proxy**. pretix needs to deliver some static content to your users (e.g. CSS, images, ...). While pretix
is capable of doing this, having this handled by a proper web server like **nginx** or **Apache** will be much
faster. Also, you need a proxying web server in front to provide SSL encryption.
diff --git a/doc/admin/installation/manual_smallscale.rst b/doc/admin/installation/manual_smallscale.rst
index 816ba0fa08..ac981934f2 100644
--- a/doc/admin/installation/manual_smallscale.rst
+++ b/doc/admin/installation/manual_smallscale.rst
@@ -23,7 +23,7 @@ installation guides):
* A SMTP server to send out mails, e.g. `Postfix`_ on your machine or some third-party server you have credentials for
* A HTTP reverse proxy, e.g. `nginx`_ or Apache to allow HTTPS connections
-* A `MySQL`_ or `PostgreSQL`_ database server
+* A `PostgreSQL`_, `MySQL`_ 5.7+, or MariaDB 10.2.7+ database server
* A `redis`_ server
We also recommend that you use a firewall, although this is not a pretix-specific recommendation. If you're new to
@@ -33,6 +33,9 @@ Linux and firewalls, we recommend that you start with `ufw`_.
SSL certificates can be obtained for free these days. We also *do not* provide support for HTTP-only
installations except for evaluation purposes.
+.. warning:: We recommend **PostgreSQL**. If you go for MySQL, make sure you run **MySQL 5.7 or newer** or
+ **MariaDB 10.2.7 or newer**.
+
Unix user
---------
@@ -50,7 +53,7 @@ Having the database server installed, we still need a database and a database us
of database managing tool or directly on our database's shell, e.g. for MySQL::
$ mysql -u root -p
- mysql> CREATE DATABASE pretix DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;
+ mysql> CREATE DATABASE pretix DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
mysql> GRANT ALL PRIVILEGES ON pretix.* TO pretix@'localhost' IDENTIFIED BY '*********';
mysql> FLUSH PRIVILEGES;
diff --git a/doc/api/resources/carts.rst b/doc/api/resources/carts.rst
index dc4cfe344f..fdf36b0fea 100644
--- a/doc/api/resources/carts.rst
+++ b/doc/api/resources/carts.rst
@@ -25,6 +25,7 @@ item integer ID of the item
variation integer ID of the variation (or ``null``)
price money (string) Price of this position
attendee_name string Specified attendee name for this position (or ``null``)
+attendee_name_parts object of strings Composition of attendee name (i.e. first name, last name, …)
attendee_email string Specified attendee email address for this position (or ``null``)
voucher integer Internal ID of the voucher used for this position (or ``null``)
addon_to integer Internal ID of the position this position is an add-on for (or ``null``)
@@ -78,6 +79,7 @@ Cart position endpoints
"variation": null,
"price": "23.00",
"attendee_name": null,
+ "attendee_name_parts": {},
"attendee_email": null,
"voucher": null,
"addon_to": null,
@@ -122,6 +124,7 @@ Cart position endpoints
"variation": null,
"price": "23.00",
"attendee_name": null,
+ "attendee_name_parts": {},
"attendee_email": null,
"voucher": null,
"addon_to": null,
@@ -175,7 +178,7 @@ Cart position endpoints
* ``item``
* ``variation`` (optional)
* ``price``
- * ``attendee_name`` (optional)
+ * ``attendee_name`` **or** ``attendee_name_parts`` (optional)
* ``attendee_email`` (optional)
* ``subevent`` (optional)
* ``expires`` (optional)
@@ -199,7 +202,10 @@ Cart position endpoints
"item": 1,
"variation": null,
"price": "23.00",
- "attendee_name": "Peter",
+ "attendee_name_parts": {
+ "given_name": "Peter",
+ "family_name": "Miller"
+ },
"attendee_email": null,
"answers": [
{
diff --git a/doc/api/resources/checkinlists.rst b/doc/api/resources/checkinlists.rst
index b883380426..fa6f006e29 100644
--- a/doc/api/resources/checkinlists.rst
+++ b/doc/api/resources/checkinlists.rst
@@ -371,6 +371,9 @@ Order position endpoints
"variation": null,
"price": "23.00",
"attendee_name": "Peter",
+ "attendee_name_parts": {
+ "full_name": "Peter",
+ },
"attendee_email": null,
"voucher": null,
"tax_rate": "0.00",
@@ -466,6 +469,9 @@ Order position endpoints
"variation": null,
"price": "23.00",
"attendee_name": "Peter",
+ "attendee_name_parts": {
+ "full_name": "Peter",
+ },
"attendee_email": null,
"voucher": null,
"tax_rate": "0.00",
diff --git a/doc/api/resources/orders.rst b/doc/api/resources/orders.rst
index bfc40954fb..b5d8096d8a 100644
--- a/doc/api/resources/orders.rst
+++ b/doc/api/resources/orders.rst
@@ -46,6 +46,7 @@ invoice_address object Invoice address
for orders created before pretix 1.7, do not rely on
it).
├ name string Customer name
+├ name_parts object of strings Customer name decomposition
├ street string Customer street
├ zipcode string Customer ZIP code
├ city string Customer city
@@ -137,6 +138,7 @@ item integer ID of the purch
variation integer ID of the purchased variation (or ``null``)
price money (string) Price of this position
attendee_name string Specified attendee name for this position (or ``null``)
+attendee_name_parts object of strings Decomposition of attendee name (i.e. given name, family name)
attendee_email string Specified attendee email address for this position (or ``null``)
voucher integer Internal ID of the voucher used for this position (or ``null``)
tax_rate decimal (string) VAT rate applied for this position
@@ -278,6 +280,7 @@ List of all orders
"is_business": True,
"company": "Sample company",
"name": "John Doe",
+ "name_parts": {"full_name": "John Doe"},
"street": "Test street 12",
"zipcode": "12345",
"city": "Testington",
@@ -295,6 +298,9 @@ List of all orders
"variation": null,
"price": "23.00",
"attendee_name": "Peter",
+ "attendee_name_parts": {
+ "full_name": "Peter",
+ },
"attendee_email": null,
"voucher": null,
"tax_rate": "0.00",
@@ -410,6 +416,7 @@ Fetching individual orders
"company": "Sample company",
"is_business": True,
"name": "John Doe",
+ "name_parts": {"full_name": "John Doe"},
"street": "Test street 12",
"zipcode": "12345",
"city": "Testington",
@@ -427,6 +434,9 @@ Fetching individual orders
"variation": null,
"price": "23.00",
"attendee_name": "Peter",
+ "attendee_name_parts": {
+ "full_name": "Peter",
+ },
"attendee_email": null,
"voucher": null,
"tax_rate": "0.00",
@@ -601,7 +611,7 @@ Creating orders
* ``company``
* ``is_business``
- * ``name``
+ * ``name`` **or** ``name_parts``
* ``street``
* ``zipcode``
* ``city``
@@ -615,7 +625,7 @@ Creating orders
* ``item``
* ``variation``
* ``price``
- * ``attendee_name``
+ * ``attendee_name`` **or** ``attendee_name_parts``
* ``attendee_email``
* ``secret`` (optional)
* ``addon_to`` (optional, see below)
@@ -664,7 +674,7 @@ Creating orders
"invoice_address": {
"is_business": False,
"company": "Sample company",
- "name": "John Doe",
+ "name_parts": {"full_name": "John Doe"},
"street": "Sesam Street 12",
"zipcode": "12345",
"city": "Sample City",
@@ -678,7 +688,9 @@ Creating orders
"item": 1,
"variation": null,
"price": "23.00",
- "attendee_name": "Peter",
+ "attendee_name_parts": {
+ "full_name": "Peter"
+ },
"attendee_email": null,
"addon_to": null,
"answers": [
@@ -1075,6 +1087,9 @@ List of all order positions
"variation": null,
"price": "23.00",
"attendee_name": "Peter",
+ "attendee_name_parts": {
+ "full_name": "Peter"
+ },
"attendee_email": null,
"voucher": null,
"tax_rate": "0.00",
@@ -1172,6 +1187,9 @@ Fetching individual positions
"variation": null,
"price": "23.00",
"attendee_name": "Peter",
+ "attendee_name_parts": {
+ "full_name": "Peter",
+ },
"attendee_email": null,
"voucher": null,
"tax_rate": "0.00",
diff --git a/src/pretix/api/serializers/cart.py b/src/pretix/api/serializers/cart.py
index 6a2b4303be..e0bbd13e44 100644
--- a/src/pretix/api/serializers/cart.py
+++ b/src/pretix/api/serializers/cart.py
@@ -19,18 +19,19 @@ class CartPositionSerializer(I18nAwareModelSerializer):
class Meta:
model = CartPosition
- fields = ('id', 'cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_email',
- 'voucher', 'addon_to', 'subevent', 'datetime', 'expires', 'includes_tax',
+ fields = ('id', 'cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
+ 'attendee_email', 'voucher', 'addon_to', 'subevent', 'datetime', 'expires', 'includes_tax',
'answers',)
class CartPositionCreateSerializer(I18nAwareModelSerializer):
answers = AnswerCreateSerializer(many=True, required=False)
expires = serializers.DateTimeField(required=False)
+ attendee_name = serializers.CharField(required=False)
class Meta:
model = CartPosition
- fields = ('cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_email',
+ fields = ('cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
'subevent', 'expires', 'includes_tax', 'answers',)
def create(self, validated_data):
@@ -65,6 +66,11 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
quota.name
)
)
+ attendee_name = validated_data.pop('attendee_name', '')
+ if attendee_name and not validated_data.get('attendee_name_parts'):
+ validated_data['attendee_name_parts'] = {
+ '_legacy': attendee_name
+ }
cp = CartPosition.objects.create(event=self.context['event'], **validated_data)
for answ_data in answers_data:
@@ -118,4 +124,8 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
raise ValidationError(
'You cannot specify a variation for this item.'
)
+ if data.get('attendee_name') and data.get('attendee_name_parts'):
+ raise ValidationError(
+ {'attendee_name': ['Do not specify attendee_name if you specified attendee_name_parts.']}
+ )
return data
diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py
index ea4c4e951d..169b45f620 100644
--- a/src/pretix/api/serializers/order.py
+++ b/src/pretix/api/serializers/order.py
@@ -35,11 +35,12 @@ class CompatibleCountryField(serializers.Field):
class InvoiceAddressSerializer(I18nAwareModelSerializer):
country = CompatibleCountryField(source='*')
+ name = serializers.CharField(required=False)
class Meta:
model = InvoiceAddress
- fields = ('last_modified', 'is_business', 'company', 'name', 'street', 'zipcode', 'city', 'country', 'vat_id',
- 'vat_id_validated', 'internal_reference')
+ fields = ('last_modified', 'is_business', 'company', 'name', 'name_parts', 'street', 'zipcode', 'city', 'country',
+ 'vat_id', 'vat_id_validated', 'internal_reference')
read_only_fields = ('last_modified', 'vat_id_validated')
def __init__(self, *args, **kwargs):
@@ -48,6 +49,15 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
v.required = False
v.allow_blank = True
+ def validate(self, data):
+ if data.get('name') and data.get('name_parts'):
+ raise ValidationError(
+ {'name': ['Do not specify name if you specified name_parts.']}
+ )
+ if data.get('name_parts') and '_scheme' not in data.get('name_parts'):
+ data['name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
+ return data
+
class AnswerQuestionIdentifierField(serializers.Field):
def to_representation(self, instance: QuestionAnswer):
@@ -158,9 +168,9 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
class Meta:
model = OrderPosition
- fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_email',
- 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins', 'downloads',
- 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data')
+ fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
+ 'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
+ 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -305,10 +315,11 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
answers = AnswerCreateSerializer(many=True, required=False)
addon_to = serializers.IntegerField(required=False, allow_null=True)
secret = serializers.CharField(required=False)
+ attendee_name = serializers.CharField(required=False)
class Meta:
model = OrderPosition
- fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_email',
+ fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
'secret', 'addon_to', 'subevent', 'answers')
def validate_secret(self, secret):
@@ -359,6 +370,12 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
raise ValidationError(
{'variation': ['You cannot specify a variation for this item.']}
)
+ if data.get('attendee_name') and data.get('attendee_name_parts'):
+ raise ValidationError(
+ {'attendee_name': ['Do not specify attendee_name if you specified attendee_name_parts.']}
+ )
+ if data.get('attendee_name_parts') and '_scheme' not in data.get('attendee_name_parts'):
+ data['attendee_name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
return data
@@ -464,7 +481,13 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
payment_info = validated_data.pop('payment_info', '{}')
if 'invoice_address' in validated_data:
- ia = InvoiceAddress(**validated_data.pop('invoice_address'))
+ iadata = validated_data.pop('invoice_address')
+ name = iadata.pop('name', '')
+ if name and not iadata.get('name_parts'):
+ iadata['name_parts'] = {
+ '_legacy': name
+ }
+ ia = InvoiceAddress(**iadata)
else:
ia = None
@@ -555,6 +578,11 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
for pos_data in positions_data:
answers_data = pos_data.pop('answers', [])
addon_to = pos_data.pop('addon_to', None)
+ attendee_name = pos_data.pop('attendee_name', '')
+ if attendee_name and not pos_data.get('attendee_name_parts'):
+ pos_data['attendee_name_parts'] = {
+ '_legacy': attendee_name
+ }
pos = OrderPosition(**pos_data)
pos.order = order
pos._calculate_tax()
diff --git a/src/pretix/api/views/checkin.py b/src/pretix/api/views/checkin.py
index 1bb3171762..2baeb642e1 100644
--- a/src/pretix/api/views/checkin.py
+++ b/src/pretix/api/views/checkin.py
@@ -154,7 +154,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = OrderPositionSerializer
queryset = OrderPosition.objects.none()
filter_backends = (DjangoFilterBackend, RichOrderingFilter)
- ordering = ('attendee_name', 'positionid')
+ ordering = ('attendee_name_cached', 'positionid')
ordering_fields = (
'order__code', 'order__datetime', 'positionid', 'attendee_name',
'last_checked_in', 'order__email',
@@ -162,11 +162,11 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
ordering_custom = {
'attendee_name': {
'_order': F('display_name').asc(nulls_first=True),
- 'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')
+ 'display_name': Coalesce('attendee_name_cached', 'addon_to__attendee_name_cached')
},
'-attendee_name': {
'_order': F('display_name').desc(nulls_last=True),
- 'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')
+ 'display_name': Coalesce('attendee_name_cached', 'addon_to__attendee_name_cached')
},
'last_checked_in': {
'_order': FixedOrderBy(F('last_checked_in'), nulls_first=True),
diff --git a/src/pretix/api/views/order.py b/src/pretix/api/views/order.py
index b1be284a1f..846d5b73f2 100644
--- a/src/pretix/api/views/order.py
+++ b/src/pretix/api/views/order.py
@@ -3,8 +3,8 @@ import datetime
import django_filters
import pytz
from django.db import transaction
-from django.db.models import Prefetch, Q
-from django.db.models.functions import Concat
+from django.db.models import F, Prefetch, Q
+from django.db.models.functions import Coalesce, Concat
from django.http import FileResponse
from django.shortcuts import get_object_or_404
from django.utils.timezone import make_aware, now
@@ -373,17 +373,17 @@ class OrderPositionFilter(FilterSet):
def search_qs(self, queryset, name, value):
return queryset.filter(
Q(secret__istartswith=value)
- | Q(attendee_name__icontains=value)
- | Q(addon_to__attendee_name__icontains=value)
+ | Q(attendee_name_cached__icontains=value)
+ | Q(addon_to__attendee_name_cached__icontains=value)
| Q(order__code__istartswith=value)
- | Q(order__invoice_address__name__icontains=value)
+ | Q(order__invoice_address__name_cached__icontains=value)
)
def has_checkin_qs(self, queryset, name, value):
return queryset.filter(checkins__isnull=not value)
def attendee_name_qs(self, queryset, name, value):
- return queryset.filter(Q(attendee_name__iexact=value) | Q(addon_to__attendee_name__iexact=value))
+ return queryset.filter(Q(attendee_name_cached__iexact=value) | Q(addon_to__attendee_name_cached__iexact=value))
class Meta:
model = OrderPosition
@@ -409,6 +409,16 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
filterset_class = OrderPositionFilter
permission = 'can_view_orders'
write_permission = 'can_change_orders'
+ ordering_custom = {
+ 'attendee_name': {
+ '_order': F('display_name').asc(nulls_first=True),
+ 'display_name': Coalesce('attendee_name_cached', 'addon_to__attendee_name_cached')
+ },
+ '-attendee_name': {
+ '_order': F('display_name').asc(nulls_last=True),
+ 'display_name': Coalesce('attendee_name_cached', 'addon_to__attendee_name_cached')
+ },
+ }
def get_queryset(self):
return OrderPosition.objects.filter(order__event=self.request.event).prefetch_related(
diff --git a/src/pretix/base/exporters/orderlist.py b/src/pretix/base/exporters/orderlist.py
index 0b25256bdf..a17cee959b 100644
--- a/src/pretix/base/exporters/orderlist.py
+++ b/src/pretix/base/exporters/orderlist.py
@@ -12,6 +12,7 @@ from django.utils.translation import ugettext as _, ugettext_lazy
from pretix.base.models import InvoiceAddress, Order, OrderPosition
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
+from pretix.base.settings import PERSON_NAME_SCHEMES
from ..exporter import BaseExporter
from ..signals import register_data_exporters
@@ -74,7 +75,14 @@ class OrderListExporter(BaseExporter):
headers = [
_('Order code'), _('Order total'), _('Status'), _('Email'), _('Order date'),
- _('Company'), _('Name'), _('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
+ _('Company'), _('Name'),
+ ]
+ name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
+ if len(name_scheme['fields']) > 1:
+ for k, label, w in name_scheme['fields']:
+ headers.append(label)
+ headers += [
+ _('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
_('Date of last payment'), _('Fees'), _('Order locale')
]
@@ -118,6 +126,13 @@ class OrderListExporter(BaseExporter):
row += [
order.invoice_address.company,
order.invoice_address.name,
+ ]
+ if len(name_scheme['fields']) > 1:
+ for k, label, w in name_scheme['fields']:
+ row.append(
+ order.invoice_address.name_parts.get(k, '')
+ )
+ row += [
order.invoice_address.street,
order.invoice_address.zipcode,
order.invoice_address.city,
@@ -126,7 +141,7 @@ class OrderListExporter(BaseExporter):
order.invoice_address.vat_id,
]
except InvoiceAddress.DoesNotExist:
- row += ['', '', '', '', '', '', '']
+ row += [''] * 7 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0)
row += [
order.payment_date.astimezone(tz).strftime('%Y-%m-%d') if order.payment_date else '',
diff --git a/src/pretix/base/forms/questions.py b/src/pretix/base/forms/questions.py
index d2b786c54b..4422ad8b99 100644
--- a/src/pretix/base/forms/questions.py
+++ b/src/pretix/base/forms/questions.py
@@ -1,3 +1,4 @@
+import copy
import logging
from decimal import Decimal
@@ -8,6 +9,7 @@ import vat_moss.id
from django import forms
from django.contrib import messages
from django.core.exceptions import ValidationError
+from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from pretix.base.forms.widgets import (
@@ -16,6 +18,7 @@ from pretix.base.forms.widgets import (
)
from pretix.base.models import InvoiceAddress, Question
from pretix.base.models.tax import EU_COUNTRIES
+from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.control.forms import SplitDateTimeField
from pretix.helpers.i18n import get_format_without_seconds
from pretix.presale.signals import question_form_fields
@@ -23,6 +26,103 @@ from pretix.presale.signals import question_form_fields
logger = logging.getLogger(__name__)
+class NamePartsWidget(forms.MultiWidget):
+ widget = forms.TextInput
+
+ def __init__(self, scheme: dict, field: forms.Field, attrs=None):
+ widgets = []
+ self.scheme = scheme
+ self.field = field
+ for fname, label, size in self.scheme['fields']:
+ a = copy.copy(attrs) or {}
+ a['data-fname'] = fname
+ widgets.append(self.widget(attrs=a))
+ super().__init__(widgets, attrs)
+
+ def decompress(self, value):
+ if value is None:
+ return None
+ data = []
+ for i, field in enumerate(self.scheme['fields']):
+ fname, label, size = field
+ data.append(value.get(fname, ""))
+ if '_legacy' in value and not data[-1]:
+ data[-1] = value.get('_legacy', '')
+ return data
+
+ def render(self, name: str, value, attrs=None, renderer=None) -> str:
+ if not isinstance(value, list):
+ value = self.decompress(value)
+ output = []
+ final_attrs = self.build_attrs(attrs or dict())
+ if 'required' in final_attrs:
+ del final_attrs['required']
+ id_ = final_attrs.get('id', None)
+ for i, widget in enumerate(self.widgets):
+ try:
+ widget_value = value[i]
+ except (IndexError, TypeError):
+ widget_value = None
+ if id_:
+ final_attrs = dict(
+ final_attrs,
+ id='%s_%s' % (id_, i),
+ title=self.scheme['fields'][i][1],
+ placeholder=self.scheme['fields'][i][1],
+ )
+ final_attrs['data-size'] = self.scheme['fields'][i][2]
+ output.append(widget.render(name + '_%s' % i, widget_value, final_attrs, renderer=renderer))
+ return mark_safe(self.format_output(output))
+
+ def format_output(self, rendered_widgets) -> str:
+ return '
%s
' % ''.join(rendered_widgets)
+
+
+class NamePartsFormField(forms.MultiValueField):
+ widget = NamePartsWidget
+
+ def compress(self, data_list) -> dict:
+ data = {}
+ data['_scheme'] = self.scheme_name
+ for i, value in enumerate(data_list):
+ data[self.scheme['fields'][i][0]] = value or ''
+ return data
+
+ def __init__(self, *args, **kwargs):
+ fields = []
+ defaults = {
+ 'widget': self.widget,
+ 'max_length': kwargs.pop('max_length', None),
+ }
+ self.scheme_name = kwargs.pop('scheme')
+ self.scheme = PERSON_NAME_SCHEMES.get(self.scheme_name)
+ self.one_required = kwargs.get('required', True)
+ require_all_fields = kwargs.pop('require_all_fields', False)
+ kwargs['required'] = False
+ kwargs['widget'] = (kwargs.get('widget') or self.widget)(
+ scheme=self.scheme, field=self, **kwargs.pop('widget_kwargs', {})
+ )
+ defaults.update(**kwargs)
+ for fname, label, size in self.scheme['fields']:
+ defaults['label'] = label
+ field = forms.CharField(**defaults)
+ field.part_name = fname
+ fields.append(field)
+ super().__init__(
+ fields=fields, require_all_fields=False, *args, **kwargs
+ )
+ self.require_all_fields = require_all_fields
+ self.required = self.one_required
+
+ def clean(self, value) -> dict:
+ value = super().clean(value)
+ if self.one_required and (not value or not any(v for v in value)):
+ raise forms.ValidationError(self.error_messages['required'], code='required')
+ if self.require_all_fields and not all(v for v in value):
+ raise forms.ValidationError(self.error_messages['incomplete'], code='required')
+ return value
+
+
class BaseQuestionsForm(forms.Form):
"""
This form class is responsible for asking order-related questions. This includes
@@ -47,10 +147,12 @@ class BaseQuestionsForm(forms.Form):
super().__init__(*args, **kwargs)
if item.admission and event.settings.attendee_names_asked:
- self.fields['attendee_name'] = forms.CharField(
- max_length=255, required=event.settings.attendee_names_required,
+ self.fields['attendee_name_parts'] = NamePartsFormField(
+ max_length=255,
+ required=event.settings.attendee_names_required,
+ scheme=event.settings.name_scheme,
label=_('Attendee name'),
- initial=(cartpos.attendee_name if cartpos else orderpos.attendee_name),
+ initial=(cartpos.attendee_name_parts if cartpos else orderpos.attendee_name_parts),
)
if item.admission and event.settings.attendee_emails_asked:
self.fields['attendee_email'] = forms.EmailField(
@@ -170,13 +272,12 @@ class BaseInvoiceAddressForm(forms.ModelForm):
class Meta:
model = InvoiceAddress
- fields = ('is_business', 'company', 'name', 'street', 'zipcode', 'city', 'country', 'vat_id',
+ fields = ('is_business', 'company', 'name_parts', 'street', 'zipcode', 'city', 'country', 'vat_id',
'internal_reference')
widgets = {
'is_business': BusinessBooleanRadio,
'street': forms.Textarea(attrs={'rows': 2, 'placeholder': _('Street and Number')}),
'company': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}),
- 'name': forms.TextInput(attrs={}),
'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}),
'internal_reference': forms.TextInput,
}
@@ -191,15 +292,13 @@ class BaseInvoiceAddressForm(forms.ModelForm):
super().__init__(*args, **kwargs)
if not event.settings.invoice_address_vatid:
del self.fields['vat_id']
+
if not event.settings.invoice_address_required:
for k, f in self.fields.items():
f.required = False
f.widget.is_required = False
if 'required' in f.widget.attrs:
del f.widget.attrs['required']
-
- if event.settings.invoice_name_required:
- self.fields['name'].required = True
elif event.settings.invoice_address_company_required:
self.initial['is_business'] = True
@@ -210,20 +309,34 @@ class BaseInvoiceAddressForm(forms.ModelForm):
del self.fields['company'].widget.attrs['data-display-dependency']
if 'vat_id' in self.fields:
del self.fields['vat_id'].widget.attrs['data-display-dependency']
- else:
+
+ self.fields['name_parts'] = NamePartsFormField(
+ max_length=255,
+ required=event.settings.invoice_name_required,
+ scheme=event.settings.name_scheme,
+ label=_('Name'),
+ initial=(self.instance.name_parts if self.instance else self.instance.name_parts),
+ )
+ if event.settings.invoice_address_required and not event.settings.invoice_address_company_required:
+ self.fields['name_parts'].widget.attrs['data-required-if'] = '#id_is_business_0'
+ self.fields['name_parts'].widget.attrs['data-no-required-attr'] = '1'
self.fields['company'].widget.attrs['data-required-if'] = '#id_is_business_1'
- self.fields['name'].widget.attrs['data-required-if'] = '#id_is_business_0'
def clean(self):
data = self.cleaned_data
if not data.get('is_business'):
data['company'] = ''
- if not data.get('name') and not data.get('company') and self.event.settings.invoice_address_required:
- raise ValidationError(_('You need to provide either a company name or your name.'))
+ if self.event.settings.invoice_address_required:
+ if data.get('is_business') and not data.get('company'):
+ raise ValidationError(_('You need to provide a company name.'))
+ if not data.get('is_business') and not data.get('name_parts'):
+ raise ValidationError(_('You need to provide your name.'))
if 'vat_id' in self.changed_data or not data.get('vat_id'):
self.instance.vat_id_validated = False
+ self.instance.name_parts = data.get('name_parts')
+
if self.validate_vat_id and self.instance.vat_id_validated and 'vat_id' not in self.changed_data:
pass
elif self.validate_vat_id and data.get('is_business') and data.get('country') in EU_COUNTRIES and data.get('vat_id'):
diff --git a/src/pretix/base/migrations/0001_initial.py b/src/pretix/base/migrations/0001_initial.py
index 17aae79ca1..3240da3ea5 100644
--- a/src/pretix/base/migrations/0001_initial.py
+++ b/src/pretix/base/migrations/0001_initial.py
@@ -28,7 +28,8 @@ class Migration(migrations.Migration):
('password', models.CharField(verbose_name='password', max_length=128)),
('last_login', models.DateTimeField(verbose_name='last login', blank=True, null=True)),
('is_superuser', models.BooleanField(verbose_name='superuser status', default=False, help_text='Designates that this user has all permissions without explicitly assigning them.')),
- ('email', models.EmailField(max_length=254, blank=True, unique=True, verbose_name='E-mail', null=True, db_index=True)),
+ ('email', models.EmailField(max_length=191, blank=True, unique=True, verbose_name='E-mail', null=True,
+ db_index=True)),
('givenname', models.CharField(verbose_name='Given name', max_length=255, blank=True, null=True)),
('familyname', models.CharField(verbose_name='Family name', max_length=255, blank=True, null=True)),
('is_active', models.BooleanField(verbose_name='Is active', default=True)),
diff --git a/src/pretix/base/migrations/0102_auto_20181017_0024.py b/src/pretix/base/migrations/0102_auto_20181017_0024.py
new file mode 100644
index 0000000000..4de63daa15
--- /dev/null
+++ b/src/pretix/base/migrations/0102_auto_20181017_0024.py
@@ -0,0 +1,62 @@
+# Generated by Django 2.1 on 2018-10-17 00:24
+
+import jsonfallback.fields
+from django.db import migrations
+
+
+def set_attendee_name_parts(apps, schema_editor):
+ OrderPosition = apps.get_model('pretixbase', 'OrderPosition') # noqa
+ for op in OrderPosition.objects.exclude(attendee_name_cached=None).exclude(
+ attendee_name_cached__isnull=True).iterator():
+ op.attendee_name_parts = {'_legacy': op.attendee_name_cached}
+ CartPosition = apps.get_model('pretixbase', 'CartPosition') # noqa
+ for op in CartPosition.objects.exclude(attendee_name_cached=None).exclude(
+ attendee_name_cached__isnull=True).iterator():
+ op.attendee_name_parts = {'_legacy': op.attendee_name_cached}
+ InvoiceAddress = apps.get_model('pretixbase', 'InvoiceAddress') # noqa
+ for ia in InvoiceAddress.objects.exclude(name_cached=None).exclude(
+ name_cached__isnull=True).iterator():
+ op.name_parts = {'_legacy': ia.name_cached}
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('pretixbase', '0101_auto_20181025_2255'),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name='cartposition',
+ old_name='attendee_name',
+ new_name='attendee_name_cached',
+ ),
+ migrations.RenameField(
+ model_name='orderposition',
+ old_name='attendee_name',
+ new_name='attendee_name_cached',
+ ),
+ migrations.RenameField(
+ model_name='invoiceaddress',
+ old_name='name',
+ new_name='name_cached',
+ ),
+ migrations.AddField(
+ model_name='cartposition',
+ name='attendee_name_parts',
+ field=jsonfallback.fields.FallbackJSONField(null=False, default=dict),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name='orderposition',
+ name='attendee_name_parts',
+ field=jsonfallback.fields.FallbackJSONField(null=False, default=dict),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name='invoiceaddress',
+ name='name_parts',
+ field=jsonfallback.fields.FallbackJSONField(default=dict),
+ preserve_default=False,
+ ),
+ migrations.RunPython(set_attendee_name_parts, migrations.RunPython.noop)
+ ]
diff --git a/src/pretix/base/models/auth.py b/src/pretix/base/models/auth.py
index eaed7e10f4..ffc0daec31 100644
--- a/src/pretix/base/models/auth.py
+++ b/src/pretix/base/models/auth.py
@@ -75,7 +75,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
REQUIRED_FIELDS = []
email = models.EmailField(unique=True, db_index=True, null=True, blank=True,
- verbose_name=_('E-mail'))
+ verbose_name=_('E-mail'), max_length=190)
fullname = models.CharField(max_length=255, blank=True, null=True,
verbose_name=_('Full name'))
is_active = models.BooleanField(default=True,
diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py
index 080196924b..ed00d54b2e 100644
--- a/src/pretix/base/models/orders.py
+++ b/src/pretix/base/models/orders.py
@@ -26,10 +26,12 @@ from django.utils.timezone import make_aware, now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django_countries.fields import CountryField
from i18nfield.strings import LazyI18nString
+from jsonfallback.fields import FallbackJSONField
from pretix.base.i18n import language
from pretix.base.models import User
from pretix.base.reldate import RelativeDateWrapper
+from pretix.base.settings import PERSON_NAME_SCHEMES
from .base import LockModel, LoggedModel
from .event import Event, SubEvent
@@ -699,8 +701,10 @@ class AbstractPosition(models.Model):
:type expires: datetime
:param price: The price of this item
:type price: decimal.Decimal
- :param attendee_name: The attendee's name, if entered.
- :type attendee_name: str
+ :param attendee_name_parts: The parts of the attendee's name, if entered.
+ :type attendee_name_parts: str
+ :param attendee_name_cached: The concatenated version of the attendee's name, if entered.
+ :type attendee_name_cached: str
:param attendee_email: The attendee's email, if entered.
:type attendee_email: str
:param voucher: A voucher that has been applied to this sale
@@ -729,12 +733,15 @@ class AbstractPosition(models.Model):
decimal_places=2, max_digits=10,
verbose_name=_("Price")
)
- attendee_name = models.CharField(
+ attendee_name_cached = models.CharField(
max_length=255,
verbose_name=_("Attendee name"),
blank=True, null=True,
help_text=_("Empty, if this product is not an admission ticket")
)
+ attendee_name_parts = FallbackJSONField(
+ blank=True, default=dict
+ )
attendee_email = models.EmailField(
verbose_name=_("Attendee email"),
blank=True, null=True,
@@ -797,6 +804,24 @@ class AbstractPosition(models.Model):
if self.variation is None
else self.variation.quotas.filter(subevent=self.subevent))
+ def save(self, *args, **kwargs):
+ self.attendee_name_cached = self.attendee_name
+ if self.attendee_name_parts is None:
+ self.attendee_name_parts = {}
+ super().save(*args, **kwargs)
+
+ @property
+ def attendee_name(self):
+ if not self.attendee_name_parts:
+ return None
+ if '_legacy' in self.attendee_name_parts:
+ return self.attendee_name_parts['_legacy']
+ if '_scheme' in self.attendee_name_parts:
+ scheme = PERSON_NAME_SCHEMES[self.attendee_name_parts['_scheme']]
+ else:
+ scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
+ return scheme['concatenation'](self.attendee_name_parts).strip()
+
class OrderPayment(models.Model):
"""
@@ -1482,6 +1507,10 @@ class OrderPosition(AbstractPosition):
self.pseudonymization_id = code
return
+ @property
+ def event(self):
+ return self.order.event
+
class CartPosition(AbstractPosition):
"""
@@ -1547,7 +1576,8 @@ class InvoiceAddress(models.Model):
order = models.OneToOneField(Order, null=True, blank=True, related_name='invoice_address', on_delete=models.CASCADE)
is_business = models.BooleanField(default=False, verbose_name=_('Business customer'))
company = models.CharField(max_length=255, blank=True, verbose_name=_('Company name'))
- name = models.CharField(max_length=255, verbose_name=_('Full name'), blank=True)
+ name_cached = models.CharField(max_length=255, verbose_name=_('Full name'), blank=True)
+ name_parts = FallbackJSONField(default=dict)
street = models.TextField(verbose_name=_('Address'), blank=False)
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=False)
city = models.CharField(max_length=255, verbose_name=_('City'), blank=False)
@@ -1565,8 +1595,25 @@ class InvoiceAddress(models.Model):
def save(self, **kwargs):
if self.order:
self.order.touch()
+
+ if self.name_parts:
+ self.name_cached = self.name
+ else:
+ self.name_cached = ""
super().save(**kwargs)
+ @property
+ def name(self):
+ if not self.name_parts:
+ return ""
+ if '_legacy' in self.name_parts:
+ return self.name_parts['_legacy']
+ if '_scheme' in self.name_parts:
+ scheme = PERSON_NAME_SCHEMES[self.name_parts['_scheme']]
+ else:
+ raise TypeError("Invalid name given.")
+ return scheme['concatenation'](self.name_parts).strip()
+
def cachedticket_name(instance, filename: str) -> str:
secret = get_random_string(length=16, allowed_chars=string.ascii_letters + string.digits)
diff --git a/src/pretix/base/pdf.py b/src/pretix/base/pdf.py
index 791d8735c2..35bdc50b4c 100644
--- a/src/pretix/base/pdf.py
+++ b/src/pretix/base/pdf.py
@@ -26,6 +26,7 @@ from reportlab.platypus import Paragraph
from pretix.base.invoice import ThumbnailingImageReader
from pretix.base.models import Order, OrderPosition
+from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import layout_text_variables
from pretix.base.templatetags.money import money_filter
from pretix.presale.style import get_fonts
@@ -147,12 +148,12 @@ DEFAULT_VARIABLES = OrderedDict((
"evaluate": lambda op, order, ev: str(ev.location).replace("\n", "
\n")
}),
("invoice_name", {
- "label": _("Invoice address: name"),
+ "label": _("Invoice address name"),
"editor_sample": _("John Doe"),
"evaluate": lambda op, order, ev: order.invoice_address.name if getattr(order, 'invoice_address', None) else ''
}),
("invoice_company", {
- "label": _("Invoice address: company"),
+ "label": _("Invoice address company"),
"editor_sample": _("Sample company"),
"evaluate": lambda op, order, ev: order.invoice_address.company if getattr(order, 'invoice_address', None) else ''
}),
@@ -182,8 +183,28 @@ DEFAULT_VARIABLES = OrderedDict((
def get_variables(event):
v = copy.copy(DEFAULT_VARIABLES)
+
+ scheme = PERSON_NAME_SCHEMES[event.settings.name_scheme]
+ for key, label, weight in scheme['fields']:
+ v['attendee_name_%s' % key] = {
+ 'label': _("Attendee name: {part}").format(part=label),
+ 'editor_sample': scheme['sample'][key],
+ 'evaluate': lambda op, order, ev: op.attendee_name_parts.get(key, '')
+ }
+
+ v['invoice_name']['editor_sample'] = scheme['concatenation'](scheme['sample'])
+ v['attendee_name']['editor_sample'] = scheme['concatenation'](scheme['sample'])
+
+ for key, label, weight in scheme['fields']:
+ v['invoice_name_%s' % key] = {
+ 'label': _("Invoice address name: {part}").format(part=label),
+ 'editor_sample': scheme['sample'][key],
+ "evaluate": lambda op, order, ev: order.invoice_address.name_parts.get(key, '') if getattr(order, 'invoice_address', None) else ''
+ }
+
for recv, res in layout_text_variables.send(sender=event):
v.update(res)
+
return v
diff --git a/src/pretix/base/services/tickets.py b/src/pretix/base/services/tickets.py
index 5f2bb170b2..b28550bd7a 100644
--- a/src/pretix/base/services/tickets.py
+++ b/src/pretix/base/services/tickets.py
@@ -12,6 +12,7 @@ from pretix.base.models import (
OrderPosition,
)
from pretix.base.services.tasks import ProfiledTask
+from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import allow_ticket_download, register_ticket_outputs
from pretix.celery_app import app
from pretix.helpers.database import rolledback_transaction
@@ -87,11 +88,13 @@ def preview(event: int, provider: str):
locale=event.settings.locale,
expires=now(), code="PREVIEW1234", total=119)
- p = order.positions.create(item=item, attendee_name=_("John Doe"), price=item.default_price)
- order.positions.create(item=item2, attendee_name=_("John Doe"), price=item.default_price, addon_to=p)
- order.positions.create(item=item2, attendee_name=_("John Doe"), price=item.default_price, addon_to=p)
+ scheme = PERSON_NAME_SCHEMES[event.settings.name_scheme]
+ sample = {k: str(v) for k, v in scheme['sample'].items()}
+ p = order.positions.create(item=item, attendee_name_parts=sample, price=item.default_price)
+ order.positions.create(item=item2, attendee_name_parts=sample, price=item.default_price, addon_to=p)
+ order.positions.create(item=item2, attendee_name_parts=sample, price=item.default_price, addon_to=p)
- InvoiceAddress.objects.create(order=order, name=_("John Doe"), company=_("Sample company"))
+ InvoiceAddress.objects.create(order=order, name_parts=sample, company=_("Sample company"))
responses = register_ticket_outputs.send(event)
for receiver, response in responses:
diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py
index e412690e49..cbc2a8a3eb 100644
--- a/src/pretix/base/settings.py
+++ b/src/pretix/base/settings.py
@@ -1,11 +1,14 @@
import json
+from collections import OrderedDict
from datetime import datetime
from typing import Any
from django.conf import settings
from django.core.files import File
from django.db.models import Model
-from django.utils.translation import ugettext_noop
+from django.utils.translation import (
+ pgettext_lazy, ugettext_lazy as _, ugettext_noop,
+)
from hierarkey.models import GlobalSettingsBase, Hierarkey
from i18nfield.strings import LazyI18nString
@@ -556,7 +559,144 @@ Your {event} team"""))
'default': 'date_ascending',
'type': str
},
+ 'name_scheme': {
+ 'default': 'full',
+ 'type': str
+ }
}
+PERSON_NAME_SCHEMES = OrderedDict([
+ ('given_family', {
+ 'fields': (
+ ('given_name', _('Given name'), 1),
+ ('family_name', _('Family name'), 1),
+ ),
+ 'concatenation': lambda d: ' '.join(str(p) for p in [d.get('given_name', ''), d.get('family_name', '')] if p),
+ 'sample': {
+ 'given_name': pgettext_lazy('person_name_sample', 'John'),
+ 'family_name': pgettext_lazy('person_name_sample', 'Doe'),
+ },
+ }),
+ ('title_given_family', {
+ 'fields': (
+ ('title', pgettext_lazy('person_name', 'Title'), 1),
+ ('given_name', _('Given name'), 2),
+ ('family_name', _('Family name'), 2),
+ ),
+ 'concatenation': lambda d: ' '.join(
+ str(p) for p in [d.get('title', ''), d.get('given_name', ''), d.get('family_name', '')] if p
+ ),
+ 'sample': {
+ 'title': pgettext_lazy('person_name_sample', 'Dr'),
+ 'given_name': pgettext_lazy('person_name_sample', 'John'),
+ 'family_name': pgettext_lazy('person_name_sample', 'Doe'),
+ },
+ }),
+ ('given_middle_family', {
+ 'fields': (
+ ('given_name', _('First name'), 2),
+ ('middle_name', _('Middle name'), 1),
+ ('family_name', _('Family name'), 2),
+ ),
+ 'concatenation': lambda d: ' '.join(
+ str(p) for p in [d.get('given_name', ''), d.get('middle_name', ''), d.get('family_name', '')] if p
+ ),
+ 'sample': {
+ 'given_name': pgettext_lazy('person_name_sample', 'John'),
+ 'middle_name': 'M',
+ 'family_name': pgettext_lazy('person_name_sample', 'Doe'),
+ },
+ }),
+ ('title_given_middle_family', {
+ 'fields': (
+ ('title', pgettext_lazy('person_name', 'Title'), 1),
+ ('given_name', _('First name'), 2),
+ ('middle_name', _('Middle name'), 1),
+ ('family_name', _('Family name'), 1),
+ ),
+ 'concatenation': lambda d: ' '.join(
+ str(p) for p in [d.get('title', ''), d.get('given_name'), d.get('middle_name'), d.get('family_name')] if p
+ ),
+ 'sample': {
+ 'title': pgettext_lazy('person_name_sample', 'Dr'),
+ 'given_name': pgettext_lazy('person_name_sample', 'John'),
+ 'middle_name': 'M',
+ 'family_name': pgettext_lazy('person_name_sample', 'Doe'),
+ },
+ }),
+ ('family_given', {
+ 'fields': (
+ ('family_name', _('Family name'), 1),
+ ('given_name', _('Given name'), 1),
+ ),
+ 'concatenation': lambda d: ' '.join(
+ str(p) for p in [d.get('family_name', ''), d.get('given_name', '')] if p
+ ),
+ 'sample': {
+ 'given_name': pgettext_lazy('person_name_sample', 'John'),
+ 'family_name': pgettext_lazy('person_name_sample', 'Doe'),
+ },
+ }),
+ ('family_nospace_given', {
+ 'fields': (
+ ('given_name', _('Given name'), 1),
+ ('family_name', _('Family name'), 1),
+ ),
+ 'concatenation': lambda d: ''.join(
+ str(p) for p in [d.get('family_name', ''), d.get('given_name', '')] if p
+ ),
+ 'sample': {
+ 'given_name': '泽东',
+ 'family_name': '毛',
+ },
+ }),
+ ('family_comma_given', {
+ 'fields': (
+ ('given_name', _('Given name'), 1),
+ ('family_name', _('Family name'), 1),
+ ),
+ 'concatenation': lambda d: (
+ str(d.get('family_name', '')) +
+ str((', ' if d.get('family_name') and d.get('given_name') else '')) +
+ str(d.get('given_name', ''))
+ ),
+ 'sample': {
+ 'given_name': pgettext_lazy('person_name_sample', 'John'),
+ 'family_name': pgettext_lazy('person_name_sample', 'Doe'),
+ },
+ }),
+ ('full', {
+ 'fields': (
+ ('full_name', _('Name'), 1),
+ ),
+ 'concatenation': lambda d: str(d.get('full_name', '')),
+ 'sample': {
+ 'full_name': pgettext_lazy('person_name_sample', 'John Doe'),
+ },
+ }),
+ ('calling_full', {
+ 'fields': (
+ ('calling_name', _('Calling name'), 1),
+ ('full_name', _('Full name'), 2),
+ ),
+ 'concatenation': lambda d: str(d.get('full_name', '')),
+ 'sample': {
+ 'full_name': pgettext_lazy('person_name_sample', 'John Doe'),
+ 'calling_name': pgettext_lazy('person_name_sample', 'John'),
+ },
+ }),
+ ('full_transcription', {
+ 'fields': (
+ ('full_name', _('Full name'), 1),
+ ('latin_transcription', _('Latin transcription'), 2),
+ ),
+ 'concatenation': lambda d: str(d.get('full_name', '')),
+ 'sample': {
+ 'full_name': '庄司',
+ 'latin_transcription': 'Shōji'
+ },
+ }),
+])
+
settings_hierarkey = Hierarkey(attribute_name='settings')
diff --git a/src/pretix/base/shredder.py b/src/pretix/base/shredder.py
index 39a2376789..4210162252 100644
--- a/src/pretix/base/shredder.py
+++ b/src/pretix/base/shredder.py
@@ -3,7 +3,7 @@ from datetime import timedelta
from typing import List, Tuple
from django.db import transaction
-from django.db.models import Max
+from django.db.models import Max, Q
from django.db.models.functions import Greatest
from django.dispatch import receiver
from django.utils.timezone import now
@@ -202,12 +202,20 @@ class AttendeeNameShredder(BaseDataShredder):
def generate_files(self) -> List[Tuple[str, str, str]]:
yield 'attendee-names.json', 'application/json', json.dumps({
'{}-{}'.format(op.order.code, op.positionid): op.attendee_name
- for op in OrderPosition.objects.filter(order__event=self.event, attendee_name__isnull=False)
+ for op in OrderPosition.objects.filter(
+ order__event=self.event
+ ).filter(
+ Q(Q(attendee_name_cached__isnull=False) | Q(attendee_name_parts__isnull=False))
+ )
}, indent=4)
@transaction.atomic
def shred_data(self):
- OrderPosition.objects.filter(order__event=self.event, attendee_name__isnull=False).update(attendee_name=None)
+ OrderPosition.objects.filter(
+ order__event=self.event
+ ).filter(
+ Q(Q(attendee_name_cached__isnull=False) | Q(attendee_name_parts__isnull=False))
+ ).update(attendee_name_cached=None, attendee_name_parts={'_shredded': True})
for le in self.event.logentry_set.filter(action_type="pretix.event.order.modified").exclude(data=""):
d = le.parsed_data
@@ -215,6 +223,10 @@ class AttendeeNameShredder(BaseDataShredder):
for i, row in enumerate(d['data']):
if 'attendee_name' in row:
d['data'][i]['attendee_name'] = '█'
+ if 'attendee_name_parts' in row:
+ d['data'][i]['attendee_name_parts'] = {
+ '_legacy': '█'
+ }
le.data = json.dumps(d)
le.shredded = True
le.save(update_fields=['data', 'shredded'])
diff --git a/src/pretix/base/views/mixins.py b/src/pretix/base/views/mixins.py
index 1092a28103..cc4ff0e0f7 100644
--- a/src/pretix/base/views/mixins.py
+++ b/src/pretix/base/views/mixins.py
@@ -80,8 +80,8 @@ class BaseQuestionsViewMixin:
# This form was correctly filled, so we store the data as
# answers to the questions / in the CartPosition object
for k, v in form.cleaned_data.items():
- if k == 'attendee_name':
- form.pos.attendee_name = v if v != '' else None
+ if k == 'attendee_name_parts':
+ form.pos.attendee_name_parts = v if v else None
form.pos.save()
elif k == 'attendee_email':
form.pos.attendee_email = v if v != '' else None
diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py
index 5b724153c5..21242ecd5f 100644
--- a/src/pretix/control/forms/event.py
+++ b/src/pretix/control/forms/event.py
@@ -20,6 +20,7 @@ from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
from pretix.base.models import Event, Organizer, TaxRule
from pretix.base.models.event import EventMetaValue, SubEvent
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
+from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.control.forms import (
ExtFileField, MultipleLanguagesWidget, SingleLanguageWidget, SlugWidget,
SplitDateTimeField, SplitDateTimePickerWidget,
@@ -338,6 +339,12 @@ class EventSettingsForm(SettingsForm):
required=False,
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_names_asked'}),
)
+ name_scheme = forms.ChoiceField(
+ label=_("Name format"),
+ help_text=_("This defines how pretix will ask for human names. Changing this after you already received "
+ "orders might lead to unexpected behaviour when sorting or changing names."),
+ required=True,
+ )
attendee_emails_asked = forms.BooleanField(
label=_("Ask for email addresses per ticket"),
help_text=_("Normally, pretix asks for one email address per order and the order confirmation will be sent "
@@ -419,6 +426,13 @@ class EventSettingsForm(SettingsForm):
'e.g. I hereby confirm that I have read and agree with the event organizer\'s terms of service '
'and agree with them.'
)
+ self.fields['name_scheme'].choices = (
+ (k, _('Ask for {fields}, display like {example}').format(
+ fields=' + '.join(str(vv[1]) for vv in v['fields']),
+ example=v['concatenation'](v['sample'])
+ ))
+ for k, v in PERSON_NAME_SCHEMES.items()
+ )
class PaymentSettingsForm(SettingsForm):
diff --git a/src/pretix/control/forms/filter.py b/src/pretix/control/forms/filter.py
index 2684e7f96a..e105dc63fa 100644
--- a/src/pretix/control/forms/filter.py
+++ b/src/pretix/control/forms/filter.py
@@ -129,7 +129,7 @@ class OrderFilterForm(FilterForm):
matching_positions = OrderPosition.objects.filter(
Q(order=OuterRef('pk')) & Q(
- Q(attendee_name__icontains=u) | Q(attendee_email__icontains=u)
+ Q(attendee_name_cached__icontains=u) | Q(attendee_email__icontains=u)
| Q(secret__istartswith=u)
)
).values('id')
@@ -137,7 +137,7 @@ class OrderFilterForm(FilterForm):
qs = qs.annotate(has_pos=Exists(matching_positions)).filter(
code
| Q(email__icontains=u)
- | Q(invoice_address__name__icontains=u)
+ | Q(invoice_address__name_cached__icontains=u)
| Q(invoice_address__company__icontains=u)
| Q(pk__in=matching_invoices)
| Q(comment__icontains=u)
@@ -568,9 +568,9 @@ class CheckInFilterForm(FilterForm):
'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')},
+ 'display_name': Coalesce('attendee_name_cached', 'addon_to__attendee_name_cached')},
'-name': {'_order': F('display_name').desc(nulls_last=True),
- 'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')},
+ 'display_name': Coalesce('attendee_name_cached', 'addon_to__attendee_name_cached')},
}
user = forms.CharField(
@@ -615,10 +615,10 @@ class CheckInFilterForm(FilterForm):
Q(order__code__istartswith=u)
| Q(secret__istartswith=u)
| Q(order__email__icontains=u)
- | Q(attendee_name__icontains=u)
+ | Q(attendee_name_cached__icontains=u)
| Q(attendee_email__icontains=u)
| Q(voucher__code__istartswith=u)
- | Q(order__invoice_address__name__icontains=u)
+ | Q(order__invoice_address__name_cached__icontains=u)
| Q(order__invoice_address__company__icontains=u)
)
diff --git a/src/pretix/control/templates/pretixcontrol/event/settings.html b/src/pretix/control/templates/pretixcontrol/event/settings.html
index 0a40970755..a4581b0a71 100644
--- a/src/pretix/control/templates/pretixcontrol/event/settings.html
+++ b/src/pretix/control/templates/pretixcontrol/event/settings.html
@@ -63,6 +63,7 @@
{% bootstrap_field sform.max_items_per_order layout="control" %}
{% bootstrap_field sform.attendee_names_asked layout="control" %}
{% bootstrap_field sform.attendee_names_required layout="control" %}
+ {% bootstrap_field sform.name_scheme layout="control" %}
{% bootstrap_field sform.order_email_asked_twice layout="control" %}
{% bootstrap_field sform.attendee_emails_asked layout="control" %}
{% bootstrap_field sform.attendee_emails_required layout="control" %}
diff --git a/src/pretix/control/views/event.py b/src/pretix/control/views/event.py
index 6c9dfc3ac1..a063b4bb35 100644
--- a/src/pretix/control/views/event.py
+++ b/src/pretix/control/views/event.py
@@ -644,7 +644,7 @@ class MailSettingsRendererPreview(MailSettingsPreview):
expires=now(), code="PREVIEW", total=119)
item = request.event.items.create(name=ugettext("Sample product"), default_price=42.23,
description=ugettext("Sample product description"))
- order.positions.create(item=item, attendee_name=ugettext("John Doe"), price=item.default_price)
+ order.positions.create(item=item, attendee_name_parts={'full_name': ugettext("John Doe")}, price=item.default_price)
v = renderers[request.GET.get('renderer')].render(
v,
str(request.event.settings.mail_text_signature),
diff --git a/src/pretix/control/views/pdf.py b/src/pretix/control/views/pdf.py
index d0f9508dc2..8cd87afa28 100644
--- a/src/pretix/control/views/pdf.py
+++ b/src/pretix/control/views/pdf.py
@@ -18,6 +18,7 @@ from django.views.generic import TemplateView
from pretix.base.i18n import language
from pretix.base.models import CachedFile, InvoiceAddress, OrderPosition
from pretix.base.pdf import get_variables
+from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.helpers.database import rolledback_transaction
from pretix.presale.style import get_fonts
@@ -65,11 +66,13 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
locale=self.request.event.settings.locale,
expires=now(), code="PREVIEW1234", total=119)
- p = order.positions.create(item=item, attendee_name=_("John Doe"), price=item.default_price)
- order.positions.create(item=item2, attendee_name=_("John Doe"), price=item.default_price, addon_to=p)
- order.positions.create(item=item2, attendee_name=_("John Doe"), price=item.default_price, addon_to=p)
+ scheme = PERSON_NAME_SCHEMES[self.request.event.settings.name_scheme]
+ sample = {k: str(v) for k, v in scheme['sample'].items()}
+ p = order.positions.create(item=item, attendee_name_parts=sample, price=item.default_price)
+ order.positions.create(item=item2, attendee_name_parts=sample, price=item.default_price, addon_to=p)
+ order.positions.create(item=item2, attendee_name_parts=sample, price=item.default_price, addon_to=p)
- InvoiceAddress.objects.create(order=order, name=_("John Doe"), company=_("Sample company"))
+ InvoiceAddress.objects.create(order=order, name_parts=sample, company=_("Sample company"))
return p
def generate(self, p: OrderPosition, override_layout=None, override_background=None):
diff --git a/src/pretix/control/views/search.py b/src/pretix/control/views/search.py
index 77eda7b516..17d380d5ba 100644
--- a/src/pretix/control/views/search.py
+++ b/src/pretix/control/views/search.py
@@ -36,7 +36,8 @@ class OrderSearch(PaginationMixin, ListView):
qs = self.filter_form.filter_qs(qs)
return qs.only(
- 'id', 'invoice_address__name', 'code', 'event', 'email', 'datetime', 'total', 'status'
+ 'id', 'invoice_address__name_cached', 'invoice_address__name_parts', 'code', 'event', 'email',
+ 'datetime', 'total', 'status'
).prefetch_related(
'event', 'event__organizer'
)
diff --git a/src/pretix/plugins/badges/exporters.py b/src/pretix/plugins/badges/exporters.py
index 220ce0df4a..9288223d65 100644
--- a/src/pretix/plugins/badges/exporters.py
+++ b/src/pretix/plugins/badges/exporters.py
@@ -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()
diff --git a/src/pretix/plugins/banktransfer/views.py b/src/pretix/plugins/banktransfer/views.py
index aa4c7c4167..8c15390843 100644
--- a/src/pretix/plugins/banktransfer/views.py
+++ b/src/pretix/plugins/banktransfer/views.py
@@ -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)
diff --git a/src/pretix/plugins/checkinlists/exporters.py b/src/pretix/plugins/checkinlists/exporters.py
index 0884143a32..86638a44c2 100644
--- a/src/pretix/plugins/checkinlists/exporters.py
+++ b/src/pretix/plugins/checkinlists/exporters.py
@@ -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 += "
" + 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')
diff --git a/src/pretix/plugins/pretixdroid/views.py b/src/pretix/plugins/pretixdroid/views.py
index 6f9b45a519..720069aca9 100644
--- a/src/pretix/plugins/pretixdroid/views.py
+++ b/src/pretix/plugins/pretixdroid/views.py
@@ -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]
diff --git a/src/pretix/plugins/ticketoutputpdf/exporters.py b/src/pretix/plugins/ticketoutputpdf/exporters.py
index cbe6c1e513..07765a0062 100644
--- a/src/pretix/plugins/ticketoutputpdf/exporters.py
+++ b/src/pretix/plugins/ticketoutputpdf/exporters.py
@@ -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:
diff --git a/src/pretix/plugins/ticketoutputpdf/migrations/0006_auto_20181017_0024.py b/src/pretix/plugins/ticketoutputpdf/migrations/0006_auto_20181017_0024.py
new file mode 100644
index 0000000000..627c8b50cf
--- /dev/null
+++ b/src/pretix/plugins/ticketoutputpdf/migrations/0006_auto_20181017_0024.py
@@ -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"}]'),
+ ),
+ ]
diff --git a/src/pretix/presale/checkoutflow.py b/src/pretix/presale/checkoutflow.py
index eb8efbcb97..c8591b7052 100644
--- a/src/pretix/presale/checkoutflow.py
+++ b/src/pretix/presale/checkoutflow.py
@@ -391,7 +391,7 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
messages.warning(request, _('Please fill in answers to all required questions.'))
return False
if cp.item.admission and self.request.event.settings.get('attendee_names_required', as_type=bool) \
- and cp.attendee_name is None:
+ and not cp.attendee_name_parts:
if warn:
messages.warning(request, _('Please fill in answers to all required questions.'))
return False
diff --git a/src/pretix/presale/forms/checkout.py b/src/pretix/presale/forms/checkout.py
index 0d2d3b8825..ee8be2dd6e 100644
--- a/src/pretix/presale/forms/checkout.py
+++ b/src/pretix/presale/forms/checkout.py
@@ -66,7 +66,7 @@ class InvoiceNameForm(InvoiceAddressForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for f in list(self.fields.keys()):
- if f != 'name':
+ if f != 'name_parts':
del self.fields[f]
diff --git a/src/pretix/presale/templates/pretixpresale/base.html b/src/pretix/presale/templates/pretixpresale/base.html
index 4da62de318..99c68986c1 100644
--- a/src/pretix/presale/templates/pretixpresale/base.html
+++ b/src/pretix/presale/templates/pretixpresale/base.html
@@ -34,7 +34,8 @@
{% endcompress %}
{{ html_head|safe }}
-
+
+
{% block custom_header %}{% endblock %}
diff --git a/src/pretix/presale/templates/pretixpresale/event/index.html b/src/pretix/presale/templates/pretixpresale/event/index.html
index 92e058c489..d1931ef10f 100644
--- a/src/pretix/presale/templates/pretixpresale/event/index.html
+++ b/src/pretix/presale/templates/pretixpresale/event/index.html
@@ -102,7 +102,7 @@
{% if frontpage_text and not cart_namespace %}
- {{ frontpage_text|rich_text }}
+ {{ frontpage_text|rich_text|linebreaksbr }}
{% endif %}
diff --git a/src/pretix/presale/views/__init__.py b/src/pretix/presale/views/__init__.py
index 4b9b38451c..1fd938b1a4 100644
--- a/src/pretix/presale/views/__init__.py
+++ b/src/pretix/presale/views/__init__.py
@@ -184,6 +184,8 @@ def get_cart(request):
'item', 'variation', 'subevent', 'subevent__event', 'subevent__event__organizer',
'item__tax_rule'
)
+ for cp in request._cart_cache:
+ cp.event = request.event # Populate field with known value to save queries
return request._cart_cache
diff --git a/src/pretix/settings.py b/src/pretix/settings.py
index 018e5d8fa6..4c1996bb6f 100644
--- a/src/pretix/settings.py
+++ b/src/pretix/settings.py
@@ -58,6 +58,8 @@ debug_fallback = "runserver" in sys.argv
DEBUG = config.getboolean('django', 'debug', fallback=debug_fallback)
db_backend = config.get('database', 'backend', fallback='sqlite3')
+if db_backend == 'postgresql_psycopg2':
+ db_backend = 'postgresql'
DATABASE_IS_GALERA = config.getboolean('database', 'galera', fallback=False)
if DATABASE_IS_GALERA and 'mysql' in db_backend:
db_options = {
@@ -66,6 +68,10 @@ if DATABASE_IS_GALERA and 'mysql' in db_backend:
else:
db_options = {}
+if 'mysql' in db_backend:
+ db_options['charset'] = 'utf8mb4'
+JSON_FIELD_AVAILABLE = db_backend in ('mysql', 'postgresql')
+
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.' + db_backend,
@@ -75,7 +81,11 @@ DATABASES = {
'HOST': config.get('database', 'host', fallback=''),
'PORT': config.get('database', 'port', fallback=''),
'CONN_MAX_AGE': 0 if db_backend == 'sqlite3' else 120,
- 'OPTIONS': db_options
+ 'OPTIONS': db_options,
+ 'TEST': {
+ 'CHARSET': 'utf8mb4',
+ 'COLLATION': 'utf8mb4_unicode_ci',
+ } if 'mysql' in db_backend else {}
}
}
diff --git a/src/pretix/static/pretixcontrol/scss/_forms.scss b/src/pretix/static/pretixcontrol/scss/_forms.scss
index 839f6d9174..cfc0949d20 100644
--- a/src/pretix/static/pretixcontrol/scss/_forms.scss
+++ b/src/pretix/static/pretixcontrol/scss/_forms.scss
@@ -366,3 +366,52 @@ table td > .checkbox input[type="checkbox"] {
box-shadow: 0 1px 3px 0 #aaa;
}
}
+
+@media(max-width: $screen-xs-max) {
+ .nameparts-form-group {
+ display: block;
+ input:not(:first-child) {
+ border-top-right-radius: 0;
+ border-top-left-radius: 0;
+ }
+ input:not(:last-child) {
+ border-bottom-right-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+ }
+}
+@media(min-width: $screen-sm-min) {
+ .nameparts-form-group {
+ display: flex;
+ flex-direction: row;
+ input {
+ width: auto;
+ flex-basis: 0;
+ flex-grow: 1;
+ }
+ input:not(:first-child) {
+ border-bottom-left-radius: 0;
+ border-top-left-radius: 0;
+ }
+ input:not(:last-child) {
+ border-bottom-right-radius: 0;
+ border-top-right-radius: 0;
+ }
+ input[data-size="1"] {
+ flex-grow: 1;
+ flex-shrink: 4;
+ }
+ input[data-size="2"] {
+ flex-grow: 2;
+ flex-shrink: 3;
+ }
+ input[data-size="3"] {
+ flex-grow: 3;
+ flex-shrink: 2;
+ }
+ input[data-size="4"] {
+ flex-grow: 4;
+ flex-shrink: 1;
+ }
+ }
+}
diff --git a/src/pretix/static/pretixpresale/js/ui/main.js b/src/pretix/static/pretixpresale/js/ui/main.js
index 8386f50602..7d311f2602 100644
--- a/src/pretix/static/pretixpresale/js/ui/main.js
+++ b/src/pretix/static/pretixpresale/js/ui/main.js
@@ -189,7 +189,10 @@ $(function () {
dependency = $($(this).attr("data-required-if")),
update = function (ev) {
var enabled = (dependency.attr("type") === 'checkbox' || dependency.attr("type") === 'radio') ? dependency.prop('checked') : !!dependency.val();
- dependent.prop('required', enabled).closest('.form-group').toggleClass('required', enabled);
+ if (!dependent.is("[data-no-required-attr]")) {
+ dependent.prop('required', enabled);
+ }
+ dependent.closest('.form-group').toggleClass('required', enabled);
};
update();
dependency.closest('.form-group').find('input[name=' + dependency.attr("name") + ']').on("change", update);
diff --git a/src/pretix/static/pretixpresale/scss/_forms.scss b/src/pretix/static/pretixpresale/scss/_forms.scss
index e6a8e7a1a6..7ab5f286a2 100644
--- a/src/pretix/static/pretixpresale/scss/_forms.scss
+++ b/src/pretix/static/pretixpresale/scss/_forms.scss
@@ -92,3 +92,52 @@
border-left: 0;
}
}
+
+@media(max-width: $screen-xs-max) {
+ .nameparts-form-group {
+ display: block;
+ input:not(:first-child) {
+ border-top-right-radius: 0;
+ border-top-left-radius: 0;
+ }
+ input:not(:last-child) {
+ border-bottom-right-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+ }
+}
+@media(min-width: $screen-sm-min) {
+ .nameparts-form-group {
+ display: flex;
+ flex-direction: row;
+ input {
+ width: auto;
+ flex-basis: 0;
+ flex-grow: 1;
+ }
+ input:not(:first-child) {
+ border-bottom-left-radius: 0;
+ border-top-left-radius: 0;
+ }
+ input:not(:last-child) {
+ border-bottom-right-radius: 0;
+ border-top-right-radius: 0;
+ }
+ input[data-size="1"] {
+ flex-grow: 1;
+ flex-shrink: 4;
+ }
+ input[data-size="2"] {
+ flex-grow: 2;
+ flex-shrink: 3;
+ }
+ input[data-size="3"] {
+ flex-grow: 3;
+ flex-shrink: 2;
+ }
+ input[data-size="4"] {
+ flex-grow: 4;
+ flex-shrink: 1;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/requirements/postgres.txt b/src/requirements/postgres.txt
deleted file mode 100644
index deab58fa3f..0000000000
--- a/src/requirements/postgres.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-psycopg2-binary
-
diff --git a/src/requirements/production.txt b/src/requirements/production.txt
index 8977744268..d336138a06 100644
--- a/src/requirements/production.txt
+++ b/src/requirements/production.txt
@@ -34,6 +34,8 @@ babel
django-i18nfield>=1.4.0
django-hijack>=2.1.10,<2.2.0
django-oauth-toolkit==1.2.*
+django-jsonfallback
+psycopg2-binary
# Stripe
stripe==2.0.*
# PayPal
diff --git a/src/setup.cfg b/src/setup.cfg
index 6f32b74c66..44ac422713 100644
--- a/src/setup.cfg
+++ b/src/setup.cfg
@@ -13,7 +13,7 @@ known_third_party = versions
known_standard_library = typing,enum,mimetypes
multi_line_output = 5
not_skip = __init__.py
-skip = make_testdata.py,wsgi.py,bootstrap,celery_app.py,settings.py
+skip = make_testdata.py,wsgi.py,bootstrap,celery_app.py,pretix/settings.py,tests/settings.py,pretix/testutils/settings.py
[tool:pytest]
DJANGO_SETTINGS_MODULE=tests.settings
diff --git a/src/setup.py b/src/setup.py
index 52957619eb..d6e2886b73 100644
--- a/src/setup.py
+++ b/src/setup.py
@@ -127,6 +127,8 @@ setup(
'chardet<3.1.0,>=3.0.2',
'mt-940==3.2',
'django-i18nfield>=1.4.0',
+ 'django-jsonfallback',
+ 'psycopg2-binary',
'vobject==0.9.*',
'pycountry',
'django-countries',
@@ -157,7 +159,6 @@ setup(
],
'memcached': ['pylibmc'],
'mysql': ['mysqlclient'],
- 'postgres': ['psycopg2-binary'],
},
packages=find_packages(exclude=['tests', 'tests.*']),
diff --git a/src/tests/api/test_cart.py b/src/tests/api/test_cart.py
index a156b69c27..c6cda1e55a 100644
--- a/src/tests/api/test_cart.py
+++ b/src/tests/api/test_cart.py
@@ -54,7 +54,8 @@ TEST_CARTPOSITION_RES = {
'item': 1,
'variation': None,
'price': '23.00',
- 'attendee_name': None,
+ 'attendee_name_parts': {'full_name': 'Peter'},
+ 'attendee_name': 'Peter',
'attendee_email': None,
'voucher': None,
'addon_to': None,
@@ -74,7 +75,7 @@ def test_cp_list(token_client, organizer, event, item, taxrule, question):
mock_now.return_value = testtime
cr = CartPosition.objects.create(
event=event, cart_id="aaa", item=item,
- price=23,
+ price=23, attendee_name_parts={'full_name': 'Peter'},
datetime=datetime.datetime(2018, 6, 11, 10, 0, 0, 0),
expires=datetime.datetime(2018, 6, 11, 10, 0, 0, 0)
)
@@ -95,7 +96,7 @@ def test_cp_list_api(token_client, organizer, event, item, taxrule, question):
mock_now.return_value = testtime
cr = CartPosition.objects.create(
event=event, cart_id="aaa@api", item=item,
- price=23,
+ price=23, attendee_name_parts={'full_name': 'Peter'},
datetime=datetime.datetime(2018, 6, 11, 10, 0, 0, 0),
expires=datetime.datetime(2018, 6, 11, 10, 0, 0, 0)
)
@@ -116,7 +117,7 @@ def test_cp_detail(token_client, organizer, event, item, taxrule, question):
mock_now.return_value = testtime
cr = CartPosition.objects.create(
event=event, cart_id="aaa@api", item=item,
- price=23,
+ price=23, attendee_name_parts={'full_name': 'Peter'},
datetime=datetime.datetime(2018, 6, 11, 10, 0, 0, 0),
expires=datetime.datetime(2018, 6, 11, 10, 0, 0, 0)
)
@@ -137,7 +138,7 @@ def test_cp_delete(token_client, organizer, event, item, taxrule, question):
mock_now.return_value = testtime
cr = CartPosition.objects.create(
event=event, cart_id="aaa@api", item=item,
- price=23,
+ price=23, attendee_name_parts={'full_name': 'Peter'},
datetime=datetime.datetime(2018, 6, 11, 10, 0, 0, 0),
expires=datetime.datetime(2018, 6, 11, 10, 0, 0, 0)
)
@@ -154,7 +155,7 @@ CARTPOS_CREATE_PAYLOAD = {
'item': 1,
'variation': None,
'price': '23.00',
- 'attendee_name': None,
+ 'attendee_name_parts': {'full_name': 'Peter'},
'attendee_email': None,
'addon_to': None,
'subevent': None,
@@ -177,6 +178,31 @@ def test_cartpos_create(token_client, organizer, event, item, quota, question):
cp = CartPosition.objects.get(pk=resp.data['id'])
assert cp.price == Decimal('23.00')
assert cp.item == item
+ assert cp.attendee_name_parts == {'full_name': 'Peter'}
+
+
+@pytest.mark.django_db
+def test_cartpos_create_legacy_name(token_client, organizer, event, item, quota, question):
+ res = copy.deepcopy(CARTPOS_CREATE_PAYLOAD)
+ res['item'] = item.pk
+ res['attendee_name'] = 'Peter'
+ resp = token_client.post(
+ '/api/v1/organizers/{}/events/{}/cartpositions/'.format(
+ organizer.slug, event.slug
+ ), format='json', data=res
+ )
+ assert resp.status_code == 400
+ del res['attendee_name_parts']
+ resp = token_client.post(
+ '/api/v1/organizers/{}/events/{}/cartpositions/'.format(
+ organizer.slug, event.slug
+ ), format='json', data=res
+ )
+ assert resp.status_code == 201
+ cp = CartPosition.objects.get(pk=resp.data['id'])
+ assert cp.price == Decimal('23.00')
+ assert cp.item == item
+ assert cp.attendee_name_parts == {'_legacy': 'Peter'}
@pytest.mark.django_db
diff --git a/src/tests/api/test_checkin.py b/src/tests/api/test_checkin.py
index d3fcb4c14c..4e740d93f8 100644
--- a/src/tests/api/test_checkin.py
+++ b/src/tests/api/test_checkin.py
@@ -50,7 +50,7 @@ def order(event, item, other_item, taxrule):
item=item,
variation=None,
price=Decimal("23"),
- attendee_name="Peter",
+ attendee_name_parts={'full_name': "Peter"},
secret="z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
pseudonymization_id="ABCDEFGHKL",
)
@@ -60,7 +60,7 @@ def order(event, item, other_item, taxrule):
item=other_item,
variation=None,
price=Decimal("23"),
- attendee_name="Michael",
+ attendee_name_parts={'full_name': "Michael"},
secret="sf4HZG73fU6kwddgjg2QOusFbYZwVKpK",
pseudonymization_id="BACDEFGHKL",
)
@@ -75,6 +75,7 @@ TEST_ORDERPOSITION1_RES = {
"variation": None,
"price": "23.00",
"attendee_name": "Peter",
+ "attendee_name_parts": {'full_name': "Peter"},
"attendee_email": None,
"voucher": None,
"tax_rate": "0.00",
@@ -97,6 +98,7 @@ TEST_ORDERPOSITION2_RES = {
"variation": None,
"price": "23.00",
"attendee_name": "Michael",
+ "attendee_name_parts": {'full_name': "Michael"},
"attendee_email": None,
"voucher": None,
"tax_rate": "0.00",
diff --git a/src/tests/api/test_events.py b/src/tests/api/test_events.py
index 6be5cacbcc..cd37b7362a 100644
--- a/src/tests/api/test_events.py
+++ b/src/tests/api/test_events.py
@@ -49,7 +49,7 @@ def order_position(item, order, taxrule, variations):
tax_rate=taxrule.rate,
tax_value=Decimal("3"),
price=Decimal("23"),
- attendee_name="Peter",
+ attendee_name_parts={'full_name': "Peter"},
secret="z3fsn8jyufm5kpk768q69gkbyr5f4h6w"
)
return op
diff --git a/src/tests/api/test_items.py b/src/tests/api/test_items.py
index 72c365a83f..6a382c1fd7 100644
--- a/src/tests/api/test_items.py
+++ b/src/tests/api/test_items.py
@@ -61,7 +61,7 @@ def order_position(item, order, taxrule, variations):
tax_rate=taxrule.rate,
tax_value=Decimal("3"),
price=Decimal("23"),
- attendee_name="Peter",
+ attendee_name_parts={'full_name': "Peter"},
secret="z3fsn8jyufm5kpk768q69gkbyr5f4h6w"
)
return op
diff --git a/src/tests/api/test_orders.py b/src/tests/api/test_orders.py
index d45a1e4e8b..a60c2fcbd6 100644
--- a/src/tests/api/test_orders.py
+++ b/src/tests/api/test_orders.py
@@ -100,7 +100,7 @@ def order(event, item, taxrule, question):
item=item,
variation=None,
price=Decimal("23"),
- attendee_name="Peter",
+ attendee_name_parts={"full_name": "Peter", "_scheme": "full"},
secret="z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
pseudonymization_id="ABCDEFGHKL",
)
@@ -115,6 +115,7 @@ TEST_ORDERPOSITION_RES = {
"item": 1,
"variation": None,
"price": "23.00",
+ "attendee_name_parts": {"full_name": "Peter", "_scheme": "full"},
"attendee_name": "Peter",
"attendee_email": None,
"voucher": None,
@@ -195,6 +196,7 @@ TEST_ORDER_RES = {
"is_business": False,
"company": "Sample company",
"name": "",
+ "name_parts": {},
"street": "",
"zipcode": "",
"city": "",
@@ -703,7 +705,7 @@ def test_orderposition_delete(token_client, organizer, event, order, item, quest
item=item,
variation=None,
price=Decimal("23"),
- attendee_name="Peter",
+ attendee_name_parts={"full_name": "Peter", "_scheme": "full"},
secret="foobar",
pseudonymization_id="BAZ",
)
@@ -1249,7 +1251,7 @@ ORDER_CREATE_PAYLOAD = {
"invoice_address": {
"is_business": False,
"company": "Sample company",
- "name": "Fo",
+ "name_parts": {"full_name": "Fo"},
"street": "Bar",
"zipcode": "",
"city": "Sample City",
@@ -1263,7 +1265,7 @@ ORDER_CREATE_PAYLOAD = {
"item": 1,
"variation": None,
"price": "23.00",
- "attendee_name": "Peter",
+ "attendee_name_parts": {"full_name": "Peter"},
"attendee_email": None,
"addon_to": None,
"answers": [
@@ -1306,10 +1308,13 @@ def test_order_create(token_client, organizer, event, item, quota, question):
assert fee.value == Decimal('0.25')
ia = o.invoice_address
assert ia.company == "Sample company"
+ assert ia.name_parts == {"full_name": "Fo", "_scheme": "full"}
+ assert ia.name_cached == "Fo"
assert o.positions.count() == 1
pos = o.positions.first()
assert pos.item == item
assert pos.price == Decimal("23.00")
+ assert pos.attendee_name_parts == {"full_name": "Peter", "_scheme": "full"}
answ = pos.answers.first()
assert answ.question == question
assert answ.answer == "S"
@@ -1332,6 +1337,54 @@ def test_order_create_invoice_address_optional(token_client, organizer, event, i
o.invoice_address
+@pytest.mark.django_db
+def test_order_create_legacy_attendee_name(token_client, organizer, event, item, quota, question):
+ res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
+ res['positions'][0]['attendee_name'] = 'Peter'
+ res['positions'][0]['item'] = item.pk
+ res['positions'][0]['answers'][0]['question'] = question.pk
+
+ resp = token_client.post(
+ '/api/v1/organizers/{}/events/{}/orders/'.format(
+ organizer.slug, event.slug
+ ), format='json', data=res
+ )
+ assert resp.status_code == 400
+ del res['positions'][0]['attendee_name_parts']
+ resp = token_client.post(
+ '/api/v1/organizers/{}/events/{}/orders/'.format(
+ organizer.slug, event.slug
+ ), format='json', data=res
+ )
+ assert resp.status_code == 201
+ o = Order.objects.get(code=resp.data['code'])
+ assert o.positions.first().attendee_name_parts == {"_legacy": "Peter"}
+
+
+@pytest.mark.django_db
+def test_order_create_legacy_invoice_name(token_client, organizer, event, item, quota, question):
+ res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
+ res['invoice_address']['name'] = 'Peter'
+ res['positions'][0]['item'] = item.pk
+ res['positions'][0]['answers'][0]['question'] = question.pk
+
+ resp = token_client.post(
+ '/api/v1/organizers/{}/events/{}/orders/'.format(
+ organizer.slug, event.slug
+ ), format='json', data=res
+ )
+ assert resp.status_code == 400
+ del res['invoice_address']['name_parts']
+ resp = token_client.post(
+ '/api/v1/organizers/{}/events/{}/orders/'.format(
+ organizer.slug, event.slug
+ ), format='json', data=res
+ )
+ assert resp.status_code == 201
+ o = Order.objects.get(code=resp.data['code'])
+ assert o.invoice_address.name_parts == {"_legacy": "Peter"}
+
+
@pytest.mark.django_db
def test_order_create_code_optional(token_client, organizer, event, item, quota, question):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
@@ -1646,7 +1699,7 @@ def test_order_create_positionids_addons(token_client, organizer, event, item, q
"item": item.pk,
"variation": None,
"price": "23.00",
- "attendee_name": "Peter",
+ "attendee_name_parts": {"full_name": "Peter"},
"attendee_email": None,
"addon_to": None,
"answers": [],
@@ -1657,7 +1710,7 @@ def test_order_create_positionids_addons(token_client, organizer, event, item, q
"item": item.pk,
"variation": None,
"price": "23.00",
- "attendee_name": "Peter",
+ "attendee_name_parts": {"full_name": "Peter"},
"attendee_email": None,
"addon_to": 1,
"answers": [],
@@ -1685,7 +1738,7 @@ def test_order_create_positionid_validation(token_client, organizer, event, item
"item": item.pk,
"variation": None,
"price": "23.00",
- "attendee_name": "Peter",
+ "attendee_name_parts": {"full_name": "Peter"},
"attendee_email": None,
"addon_to": None,
"answers": [],
@@ -1696,7 +1749,7 @@ def test_order_create_positionid_validation(token_client, organizer, event, item
"item": item.pk,
"variation": None,
"price": "23.00",
- "attendee_name": "Peter",
+ "attendee_name_parts": {"full_name": "Peter"},
"attendee_email": None,
"addon_to": 2,
"answers": [],
@@ -1727,7 +1780,7 @@ def test_order_create_positionid_validation(token_client, organizer, event, item
"item": item.pk,
"variation": None,
"price": "23.00",
- "attendee_name": "Peter",
+ "attendee_name_parts": {"full_name": "Peter"},
"attendee_email": None,
"addon_to": None,
"answers": [],
@@ -1737,7 +1790,7 @@ def test_order_create_positionid_validation(token_client, organizer, event, item
"item": item.pk,
"variation": None,
"price": "23.00",
- "attendee_name": "Peter",
+ "attendee_name_parts": {"full_name": "Peter"},
"attendee_email": None,
"addon_to": 2,
"answers": [],
@@ -1761,7 +1814,7 @@ def test_order_create_positionid_validation(token_client, organizer, event, item
"item": item.pk,
"variation": None,
"price": "23.00",
- "attendee_name": "Peter",
+ "attendee_name_parts": {"full_name": "Peter"},
"attendee_email": None,
"answers": [],
"subevent": None
@@ -1770,7 +1823,7 @@ def test_order_create_positionid_validation(token_client, organizer, event, item
"item": item.pk,
"variation": None,
"price": "23.00",
- "attendee_name": "Peter",
+ "attendee_name_parts": {"full_name": "Peter"},
"attendee_email": None,
"answers": [],
"subevent": None
@@ -1797,7 +1850,7 @@ def test_order_create_positionid_validation(token_client, organizer, event, item
"item": item.pk,
"variation": None,
"price": "23.00",
- "attendee_name": "Peter",
+ "attendee_name_parts": {"full_name": "Peter"},
"attendee_email": None,
"answers": [],
"subevent": None
@@ -1807,7 +1860,7 @@ def test_order_create_positionid_validation(token_client, organizer, event, item
"item": item.pk,
"variation": None,
"price": "23.00",
- "attendee_name": "Peter",
+ "attendee_name_parts": {"full_name": "Peter"},
"attendee_email": None,
"answers": [],
"subevent": None
@@ -2066,7 +2119,7 @@ def test_order_create_quota_validation(token_client, organizer, event, item, quo
"item": item.pk,
"variation": None,
"price": "23.00",
- "attendee_name": "Peter",
+ "attendee_name_parts": {"full_name": "Peter"},
"attendee_email": None,
"addon_to": None,
"answers": [],
@@ -2077,7 +2130,7 @@ def test_order_create_quota_validation(token_client, organizer, event, item, quo
"item": item.pk,
"variation": None,
"price": "23.00",
- "attendee_name": "Peter",
+ "attendee_name_parts": {"full_name": "Peter"},
"attendee_email": None,
"addon_to": 1,
"answers": [],
diff --git a/src/tests/base/test_invoices.py b/src/tests/base/test_invoices.py
index e892d1c639..70ecfef5e8 100644
--- a/src/tests/base/test_invoices.py
+++ b/src/tests/base/test_invoices.py
@@ -121,7 +121,10 @@ def test_address_vat_id(env):
event, order = env
event.settings.set('invoice_language', 'en')
InvoiceAddress.objects.create(company='Acme Company', street='221B Baker Street',
- name='Sherlock Holmes', zipcode='12345', city='London', country_old='UK',
+ name_parts={'full_name': 'Sherlock Holmes', '_scheme': 'full'},
+ zipcode='12345',
+ city='London',
+ country_old='UK',
country='', vat_id='UK1234567', order=order)
inv = generate_invoice(order)
assert inv.invoice_to == "Acme Company\nSherlock Holmes\n221B Baker Street\n12345 London\nUK\nVAT-ID: UK1234567"
diff --git a/src/tests/base/test_notifications.py b/src/tests/base/test_notifications.py
index e7686482b7..ff27c6e7f5 100644
--- a/src/tests/base/test_notifications.py
+++ b/src/tests/base/test_notifications.py
@@ -34,7 +34,7 @@ def order(event):
default_price=Decimal('23.00'), admission=True)
OrderPosition.objects.create(
order=o, item=ticket, variation=None,
- price=Decimal("23.00"), attendee_name="Peter", positionid=1
+ price=Decimal("23.00"), attendee_name_parts={'full_name': "Peter"}, positionid=1
)
return o
diff --git a/src/tests/base/test_orders.py b/src/tests/base/test_orders.py
index 2d07aa4607..a226751b25 100644
--- a/src/tests/base/test_orders.py
+++ b/src/tests/base/test_orders.py
@@ -309,7 +309,7 @@ class PaymentReminderTests(TestCase):
default_price=Decimal('23.00'), admission=True)
self.op1 = OrderPosition.objects.create(
order=self.order, item=self.ticket, variation=None,
- price=Decimal("23.00"), attendee_name="Peter", positionid=1
+ price=Decimal("23.00"), attendee_name_parts={'full_name': "Peter"}, positionid=1
)
djmail.outbox = []
@@ -357,7 +357,7 @@ class DownloadReminderTests(TestCase):
default_price=Decimal('23.00'), admission=True)
self.op1 = OrderPosition.objects.create(
order=self.order, item=self.ticket, variation=None,
- price=Decimal("23.00"), attendee_name="Peter", positionid=1
+ price=Decimal("23.00"), attendee_name_parts={"full_name": "Peter"}, positionid=1
)
djmail.outbox = []
@@ -411,11 +411,11 @@ class OrderChangeManagerTests(TestCase):
default_price=Decimal('12.00'))
self.op1 = OrderPosition.objects.create(
order=self.order, item=self.ticket, variation=None,
- price=Decimal("23.00"), attendee_name="Peter", positionid=1
+ price=Decimal("23.00"), attendee_name_parts={'full_name': "Peter"}, positionid=1
)
self.op2 = OrderPosition.objects.create(
order=self.order, item=self.ticket, variation=None,
- price=Decimal("23.00"), attendee_name="Dieter", positionid=2
+ price=Decimal("23.00"), attendee_name_parts={'full_name': "Dieter"}, positionid=2
)
self.ocm = OrderChangeManager(self.order, None)
self.quota = self.event.quotas.create(name='Test', size=None)
diff --git a/src/tests/base/test_payment.py b/src/tests/base/test_payment.py
index 9fd4c706cd..b2321020d7 100644
--- a/src/tests/base/test_payment.py
+++ b/src/tests/base/test_payment.py
@@ -154,11 +154,11 @@ def test_availability_date_order_relative_subevents(event):
)
OrderPosition.objects.create(
order=order, item=ticket, variation=None, subevent=se1,
- price=Decimal("23.00"), attendee_name="Peter", positionid=1
+ price=Decimal("23.00"), attendee_name_parts={'full_name': "Peter"}, positionid=1
)
OrderPosition.objects.create(
order=order, item=ticket, variation=None, subevent=se2,
- price=Decimal("23.00"), attendee_name="Dieter", positionid=2
+ price=Decimal("23.00"), attendee_name_parts={'full_name': "Dieter"}, positionid=2
)
prov = DummyPaymentProvider(event)
diff --git a/src/tests/base/test_shredders.py b/src/tests/base/test_shredders.py
index dd57ecb426..931d08f70d 100644
--- a/src/tests/base/test_shredders.py
+++ b/src/tests/base/test_shredders.py
@@ -54,7 +54,7 @@ def order(event, item):
item=item,
variation=None,
price=Decimal("14"),
- attendee_name="Peter",
+ attendee_name_parts={'full_name': "Peter", "_scheme": "full"},
attendee_email="foo@example.org"
)
return o
@@ -155,7 +155,7 @@ def test_attendee_name_shredder(event, order):
}
s.shred_data()
order.refresh_from_db()
- assert order.positions.first().attendee_name is None
+ assert not order.positions.first().attendee_name
l1.refresh_from_db()
assert 'Hans' not in l1.data
assert 'Foo' in l1.data
@@ -186,6 +186,7 @@ def test_invoice_address_shredder(event, order):
'is_business': False,
'last_modified': ia.last_modified.isoformat().replace('+00:00', 'Z'),
'name': '',
+ 'name_parts': {},
'street': '221B Baker Street',
'vat_id': '',
'vat_id_validated': False,
diff --git a/src/tests/control/test_checkins.py b/src/tests/control/test_checkins.py
index 5ef62e34ef..c857bb8dd7 100644
--- a/src/tests/control/test_checkins.py
+++ b/src/tests/control/test_checkins.py
@@ -45,7 +45,7 @@ def dashboard_env():
item=item_ticket,
variation=None,
price=Decimal("23"),
- attendee_name="Peter"
+ attendee_name_parts={"full_name": "Peter"}
)
OrderPosition.objects.create(
order=order_paid,
@@ -77,7 +77,7 @@ def test_dashboard_pending_not_count(dashboard_env):
item=dashboard_env[4],
variation=None,
price=Decimal("23"),
- attendee_name="NotPaid"
+ attendee_name_parts={'full_name': "NotPaid"}
)
assert '0/2' in c[0]['content']
@@ -149,14 +149,14 @@ def checkin_list_env():
item=item_ticket,
variation=None,
price=Decimal("23"),
- attendee_name="Pending"
+ attendee_name_parts={'full_name': "Pending"}
)
op_a1_ticket = OrderPosition.objects.create(
order=order_a1,
item=item_ticket,
variation=None,
price=Decimal("23"),
- attendee_name="A1"
+ attendee_name_parts={'full_name': "A1"}
)
op_a1_mascot = OrderPosition.objects.create(
order=order_a1,
@@ -169,14 +169,14 @@ def checkin_list_env():
item=item_ticket,
variation=None,
price=Decimal("23"),
- attendee_name="A2"
+ attendee_name_parts={'full_name': "A2"}
)
op_a3_ticket = OrderPosition.objects.create(
order=order_a3,
item=item_ticket,
variation=None,
price=Decimal("23"),
- attendee_name="a4", # a3 attendee is a4
+ attendee_name_parts={'full_name': "a4"}, # a3 attendee is a4
attendee_email="a3company@dummy.test"
)
@@ -339,14 +339,14 @@ def checkin_list_with_addon_env():
item=item_ticket,
variation=None,
price=Decimal("23"),
- attendee_name="Pending"
+ attendee_name_parts={'full_name': "Pending"}
)
op_a1_ticket = OrderPosition.objects.create(
order=order_a1,
item=item_ticket,
variation=None,
price=Decimal("23"),
- attendee_name="A1"
+ attendee_name_parts={'full_name': "A1"}
)
op_a1_workshop = OrderPosition.objects.create(
order=order_a1,
@@ -360,7 +360,7 @@ def checkin_list_with_addon_env():
item=item_ticket,
variation=None,
price=Decimal("23"),
- attendee_name="A2"
+ attendee_name_parts={'full_name': "A2"}
)
# checkin
diff --git a/src/tests/control/test_items.py b/src/tests/control/test_items.py
index fb5ef51745..bb7add5265 100644
--- a/src/tests/control/test_items.py
+++ b/src/tests/control/test_items.py
@@ -187,13 +187,13 @@ class QuestionsTest(ItemFormTest):
expires=now() + datetime.timedelta(days=10),
total=14, locale='en')
op = OrderPosition.objects.create(order=o, item=item1, variation=None, price=Decimal("14"),
- attendee_name="Peter")
+ attendee_name_parts={'full_name': "Peter"})
op.answers.create(question=c, answer='42')
op = OrderPosition.objects.create(order=o, item=item1, variation=None, price=Decimal("14"),
- attendee_name="Michael")
+ attendee_name_parts={'full_name': "Michael"})
op.answers.create(question=c, answer='42')
op = OrderPosition.objects.create(order=o, item=item1, variation=None, price=Decimal("14"),
- attendee_name="Petra")
+ attendee_name_parts={'full_name': "Petra"})
op.answers.create(question=c, answer='39')
doc = self.get_doc('/control/event/%s/%s/questions/%s/' % (self.orga1.slug, self.event1.slug, c.id))
@@ -414,7 +414,7 @@ class ItemsTest(ItemFormTest):
item=self.item1,
variation=None,
price=Decimal("14"),
- attendee_name="Peter"
+ attendee_name_parts={'full_name': "Peter"}
)
self.client.post('/control/event/%s/%s/items/%d/delete' % (self.orga1.slug, self.event1.slug, self.item1.id),
{})
diff --git a/src/tests/control/test_orders.py b/src/tests/control/test_orders.py
index 9710d60e00..9ea88654f1 100644
--- a/src/tests/control/test_orders.py
+++ b/src/tests/control/test_orders.py
@@ -51,7 +51,7 @@ def env():
item=ticket,
variation=None,
price=Decimal("14"),
- attendee_name="Peter"
+ attendee_name_parts={'full_name': "Peter", "_scheme": "full"}
)
return event, user, o, ticket
@@ -333,7 +333,7 @@ def test_order_invoice_create_ok(client, env):
def test_order_invoice_regenerate(client, env):
client.login(email='dummy@dummy.dummy', password='dummy')
i = generate_invoice(env[2])
- InvoiceAddress.objects.create(name='Foo', order=env[2])
+ InvoiceAddress.objects.create(name_parts={'full_name': 'Foo', "_scheme": "full"}, order=env[2])
env[0].settings.set('invoice_generate', 'admin')
response = client.post('/control/event/dummy/dummy/orders/FOO/invoices/%d/regenerate' % i.pk, {}, follow=True)
assert 'alert-success' in response.rendered_content
@@ -362,7 +362,7 @@ def test_order_invoice_regenerate_unknown(client, env):
def test_order_invoice_reissue(client, env):
client.login(email='dummy@dummy.dummy', password='dummy')
i = generate_invoice(env[2])
- InvoiceAddress.objects.create(name='Foo', order=env[2])
+ InvoiceAddress.objects.create(name_parts={'full_name': 'Foo', "_scheme": "full"}, order=env[2])
env[0].settings.set('invoice_generate', 'admin')
response = client.post('/control/event/dummy/dummy/orders/FOO/invoices/%d/reissue' % i.pk, {}, follow=True)
assert 'alert-success' in response.rendered_content
@@ -528,7 +528,7 @@ def test_order_extend_expired_quota_partial(client, env):
item=env[3],
variation=None,
price=Decimal("14"),
- attendee_name="Peter"
+ attendee_name_parts={'full_name': "Peter", "_scheme": "full"}
)
o.expires = now() - timedelta(days=5)
o.status = Order.STATUS_EXPIRED
@@ -745,11 +745,11 @@ class OrderChangeTests(SoupTest):
default_price=Decimal('12.00'))
self.op1 = OrderPosition.objects.create(
order=self.order, item=self.ticket, variation=None,
- price=Decimal("23.00"), attendee_name="Peter"
+ price=Decimal("23.00"), attendee_name_parts={'full_name': "Peter", "_scheme": "full"}
)
self.op2 = OrderPosition.objects.create(
order=self.order, item=self.ticket, variation=None,
- price=Decimal("23.00"), attendee_name="Dieter"
+ price=Decimal("23.00"), attendee_name_parts={'full_name': "Dieter", "_scheme": "full"}
)
self.quota = self.event.quotas.create(name="All", size=100)
self.quota.items.add(self.ticket)
diff --git a/src/tests/control/test_search.py b/src/tests/control/test_search.py
index d4da387644..a554e609f9 100644
--- a/src/tests/control/test_search.py
+++ b/src/tests/control/test_search.py
@@ -30,7 +30,7 @@ class OrderSearchTest(SoupTest):
datetime=now(), expires=now() + datetime.timedelta(days=10),
total=14, locale='en'
)
- InvoiceAddress.objects.create(order=o1, company="Test Ltd.", name="Peter Miller")
+ InvoiceAddress.objects.create(order=o1, company="Test Ltd.", name_parts={'full_name': "Peter Miller", "_scheme": "full"})
ticket1 = Item.objects.create(event=self.event1, name='Early-bird ticket',
category=None, default_price=23,
admission=True)
@@ -39,7 +39,7 @@ class OrderSearchTest(SoupTest):
item=ticket1,
variation=None,
price=Decimal("14"),
- attendee_name="Peter",
+ attendee_name_parts={'full_name': "Peter", "_scheme": "full"},
attendee_email="att@att.com"
)
@@ -57,7 +57,7 @@ class OrderSearchTest(SoupTest):
item=ticket2,
variation=None,
price=Decimal("14"),
- attendee_name="Mark"
+ attendee_name_parts={'full_name': "Mark", "_scheme": "full"}
)
self.team = Team.objects.create(organizer=self.orga1, can_view_orders=True)
diff --git a/src/tests/plugins/badges/test_pdf.py b/src/tests/plugins/badges/test_pdf.py
index 9765763e2f..4c9ecf84bd 100644
--- a/src/tests/plugins/badges/test_pdf.py
+++ b/src/tests/plugins/badges/test_pdf.py
@@ -29,11 +29,11 @@ def env():
shirt_red = ItemVariation.objects.create(item=shirt, default_price=14, value="Red")
OrderPosition.objects.create(
order=o1, item=shirt, variation=shirt_red,
- price=12, attendee_name=None, secret='1234'
+ price=12, attendee_name_parts={}, secret='1234'
)
OrderPosition.objects.create(
order=o1, item=shirt, variation=shirt_red,
- price=12, attendee_name=None, secret='5678'
+ price=12, attendee_name_parts={}, secret='5678'
)
return event, o1, shirt
diff --git a/src/tests/plugins/pretixdroid/test_simple.py b/src/tests/plugins/pretixdroid/test_simple.py
index d9528b6664..ad379edbeb 100644
--- a/src/tests/plugins/pretixdroid/test_simple.py
+++ b/src/tests/plugins/pretixdroid/test_simple.py
@@ -36,11 +36,11 @@ def env():
)
op1 = OrderPosition.objects.create(
order=o1, item=shirt, variation=shirt_red,
- price=12, attendee_name=None, secret='1234'
+ price=12, attendee_name_parts={}, secret='1234'
)
op2 = OrderPosition.objects.create(
order=o1, item=ticket,
- price=23, attendee_name="Peter", secret='5678910'
+ price=23, attendee_name_parts={"full_name": "Peter", "_scheme": "full"}, secret='5678910'
)
cl1 = event.checkin_lists.create(name="Foo", all_products=True)
cl2 = event.checkin_lists.create(name="Bar", all_products=True)
@@ -273,7 +273,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', list=env[5])
- InvoiceAddress.objects.create(order=env[2], name="John")
+ InvoiceAddress.objects.create(order=env[2], name_parts={"full_name": "John", "_scheme": "full"})
resp = client.get('/pretixdroid/api/%s/%s/search/?key=%s&query=%s' % (
env[0].organizer.slug, env[0].slug, 'abcdefg', 'John'))
jdata = json.loads(resp.content.decode("utf-8"))
diff --git a/src/tests/plugins/pretixdroid/test_subevents.py b/src/tests/plugins/pretixdroid/test_subevents.py
index 9da6b425d2..06964328f0 100644
--- a/src/tests/plugins/pretixdroid/test_subevents.py
+++ b/src/tests/plugins/pretixdroid/test_subevents.py
@@ -39,11 +39,11 @@ def env():
)
op1 = OrderPosition.objects.create(
order=o1, item=shirt, variation=shirt_red,
- price=12, attendee_name=None, secret='1234', subevent=se1
+ price=12, attendee_name_parts={}, secret='1234', subevent=se1
)
op2 = OrderPosition.objects.create(
order=o1, item=ticket,
- price=23, attendee_name="Peter", secret='5678910', subevent=se2
+ price=23, attendee_name_parts={'full_name': "Peter"}, secret='5678910', subevent=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)
diff --git a/src/tests/plugins/test_checkinlist.py b/src/tests/plugins/test_checkinlist.py
new file mode 100644
index 0000000000..bd5b210d06
--- /dev/null
+++ b/src/tests/plugins/test_checkinlist.py
@@ -0,0 +1,108 @@
+import datetime
+from decimal import Decimal
+
+import pytest
+from django.utils.timezone import now
+
+from pretix.base.models import Event, Item, Order, OrderPosition, Organizer
+from pretix.plugins.checkinlists.exporters import CSVCheckinList
+
+
+@pytest.fixture
+def event():
+ """Returns an event instance"""
+ o = Organizer.objects.create(name='Dummy', slug='dummy')
+ event = Event.objects.create(
+ organizer=o, name='Dummy', slug='dummy',
+ date_from=now(),
+ plugins='pretix.plugins.checkinlists,tests.testdummy',
+ )
+ event.settings.set('attendee_names_asked', True)
+ event.settings.set('name_scheme', 'title_given_middle_family')
+ event.settings.set('locales', ['en', 'de'])
+ event.checkin_lists.create(name="Default", all_products=True)
+
+ order_paid = Order.objects.create(
+ code='FOO', event=event, email='dummy@dummy.test',
+ status=Order.STATUS_PAID,
+ datetime=now(), expires=now() + datetime.timedelta(days=10),
+ total=33, locale='en'
+ )
+ item_ticket = Item.objects.create(event=event, name="Ticket", default_price=23, admission=True)
+ OrderPosition.objects.create(
+ order=order_paid,
+ item=item_ticket,
+ variation=None,
+ price=Decimal("23"),
+ attendee_name_parts={"title": "Mr", "given_name": "Peter", "middle_name": "A", "family_name": "Jones"},
+ secret='hutjztuxhkbtwnesv2suqv26k6ttytxx'
+ )
+ OrderPosition.objects.create(
+ order=order_paid,
+ item=item_ticket,
+ variation=None,
+ price=Decimal("13"),
+ attendee_name_parts={"title": "Mrs", "given_name": "Andrea", "middle_name": "J", "family_name": "Zulu"},
+ secret='ggsngqtnmhx74jswjngw3fk8pfwz2a7k'
+ )
+ return event
+
+
+def clean(d):
+ return d.replace("\r", "").replace("\n", "")
+
+
+@pytest.mark.django_db
+def test_csv_simple(event):
+ c = CSVCheckinList(event)
+ _, _, content = c.render({
+ 'list': event.checkin_lists.first().pk,
+ 'secrets': True,
+ 'sort': 'name',
+ 'questions': []
+ })
+ assert clean(content.decode()) == clean(""""Order code","Attendee name","Attendee name: Title","Attendee name:
+ First name","Attendee name: Middle name","Attendee name: Family name","Product","Price","Checked in","Secret",
+"E-mail"
+"FOO","Mr Peter A Jones","Mr","Peter","A","Jones","Ticket","23.00","","hutjztuxhkbtwnesv2suqv26k6ttytxx",
+"dummy@dummy.test"
+"FOO","Mrs Andrea J Zulu","Mrs","Andrea","J","Zulu","Ticket","13.00","","ggsngqtnmhx74jswjngw3fk8pfwz2a7k",
+"dummy@dummy.test"
+""")
+
+
+@pytest.mark.django_db
+def test_csv_order_by_name_parts(event): # noqa
+ from django.conf import settings
+ if not settings.JSON_FIELD_AVAILABLE:
+ raise pytest.skip("Not supported on this database")
+ c = CSVCheckinList(event)
+ _, _, content = c.render({
+ 'list': event.checkin_lists.first().pk,
+ 'secrets': True,
+ 'sort': 'name:given_name',
+ 'questions': []
+ })
+ assert clean(content.decode()) == clean(""""Order code","Attendee name","Attendee name: Title",
+"Attendee name: First name","Attendee name: Middle name","Attendee name: Family name","Product","Price",
+"Checked in","Secret","E-mail"
+"FOO","Mrs Andrea J Zulu","Mrs","Andrea","J","Zulu","Ticket","13.00","","ggsngqtnmhx74jswjngw3fk8pfwz2a7k",
+"dummy@dummy.test"
+"FOO","Mr Peter A Jones","Mr","Peter","A","Jones","Ticket","23.00","","hutjztuxhkbtwnesv2suqv26k6ttytxx",
+"dummy@dummy.test"
+""")
+ c = CSVCheckinList(event)
+ _, _, content = c.render({
+ 'list': event.checkin_lists.first().pk,
+ 'secrets': True,
+ 'sort': 'name:family_name',
+ 'questions': []
+ })
+ assert clean(content.decode()) == clean(""""Order code","Attendee name","Attendee name: Title",
+"Attendee name: First name","Attendee name: Middle name","Attendee name: Family name","Product","Price",
+"Checked in","Secret","E-mail"
+"FOO","Mr Peter A Jones","Mr","Peter","A","Jones","Ticket","23.00","","hutjztuxhkbtwnesv2suqv26k6ttytxx",
+"dummy@dummy.test"
+"FOO","Mrs Andrea J Zulu","Mrs","Andrea","J","Zulu","Ticket","13.00","","ggsngqtnmhx74jswjngw3fk8pfwz2a7k",
+"dummy@dummy.test"
+""")
diff --git a/src/tests/plugins/ticketoutputpdf/test_ticketoutputpdf.py b/src/tests/plugins/ticketoutputpdf/test_ticketoutputpdf.py
index 8d38918068..dad1c0c0e7 100644
--- a/src/tests/plugins/ticketoutputpdf/test_ticketoutputpdf.py
+++ b/src/tests/plugins/ticketoutputpdf/test_ticketoutputpdf.py
@@ -29,11 +29,11 @@ def env0():
shirt_red = ItemVariation.objects.create(item=shirt, default_price=14, value="Red")
OrderPosition.objects.create(
order=o1, item=shirt, variation=shirt_red,
- price=12, attendee_name=None, secret='1234'
+ price=12, attendee_name_parts={}, secret='1234'
)
OrderPosition.objects.create(
order=o1, item=shirt, variation=shirt_red,
- price=12, attendee_name=None, secret='5678'
+ price=12, attendee_name_parts={}, secret='5678'
)
return event, o1
diff --git a/src/tests/presale/test_checkout.py b/src/tests/presale/test_checkout.py
index 0469e517d8..eb42888721 100644
--- a/src/tests/presale/test_checkout.py
+++ b/src/tests/presale/test_checkout.py
@@ -539,11 +539,11 @@ class CheckoutTestCase(TestCase):
)
response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.rendered_content, "lxml")
- self.assertEqual(len(doc.select('input[name=%s-attendee_name]' % cr1.id)), 1)
+ self.assertEqual(len(doc.select('input[name=%s-attendee_name_parts_0]' % cr1.id)), 1)
# Not all required fields filled out, expect failure
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
- '%s-attendee_name' % cr1.id: '',
+ '%s-attendee_name_parts_0' % cr1.id: '',
'email': 'admin@localhost'
}, follow=True)
doc = BeautifulSoup(response.rendered_content, "lxml")
@@ -551,7 +551,7 @@ class CheckoutTestCase(TestCase):
# Corrected request
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
- '%s-attendee_name' % cr1.id: 'Peter',
+ '%s-attendee_name_parts_0' % cr1.id: 'Peter',
'email': 'admin@localhost'
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
@@ -560,6 +560,42 @@ class CheckoutTestCase(TestCase):
cr1 = CartPosition.objects.get(id=cr1.id)
self.assertEqual(cr1.attendee_name, 'Peter')
+ def test_attendee_name_scheme(self):
+ self.event.settings.set('attendee_names_asked', True)
+ self.event.settings.set('attendee_names_required', True)
+ self.event.settings.set('name_scheme', 'title_given_middle_family')
+ cr1 = CartPosition.objects.create(
+ event=self.event, cart_id=self.session_key, item=self.ticket,
+ price=23, expires=now() + timedelta(minutes=10)
+ )
+ response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True)
+ doc = BeautifulSoup(response.rendered_content, "lxml")
+ self.assertEqual(len(doc.select('input[name=%s-attendee_name_parts_0]' % cr1.id)), 1)
+ self.assertEqual(len(doc.select('input[name=%s-attendee_name_parts_1]' % cr1.id)), 1)
+ self.assertEqual(len(doc.select('input[name=%s-attendee_name_parts_2]' % cr1.id)), 1)
+ self.assertEqual(len(doc.select('input[name=%s-attendee_name_parts_3]' % cr1.id)), 1)
+
+ # Not all required fields filled out, expect failure
+ response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
+ '%s-attendee_name_parts_0' % cr1.id: 'Mr',
+ '%s-attendee_name_parts_1' % cr1.id: 'John',
+ '%s-attendee_name_parts_2' % cr1.id: 'F',
+ '%s-attendee_name_parts_3' % cr1.id: 'Kennedy',
+ 'email': 'admin@localhost'
+ })
+ self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
+ target_status_code=200)
+
+ cr1 = CartPosition.objects.get(id=cr1.id)
+ self.assertEqual(cr1.attendee_name, 'Mr John F Kennedy')
+ self.assertEqual(cr1.attendee_name_parts, {
+ 'given_name': 'John',
+ 'title': 'Mr',
+ 'middle_name': 'F',
+ 'family_name': 'Kennedy',
+ "_scheme": "title_given_middle_family"
+ })
+
def test_attendee_name_optional(self):
self.event.settings.set('attendee_names_asked', True)
self.event.settings.set('attendee_names_required', False)
@@ -569,22 +605,23 @@ class CheckoutTestCase(TestCase):
)
response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.rendered_content, "lxml")
- self.assertEqual(len(doc.select('input[name=%s-attendee_name]' % cr1.id)), 1)
+ self.assertEqual(len(doc.select('input[name=%s-attendee_name_parts_0]' % cr1.id)), 1)
# Not all fields filled out, expect success
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
- '%s-attendee_name' % cr1.id: '',
+ '%s-attendee_name_parts_0' % cr1.id: '',
'email': 'admin@localhost'
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
target_status_code=200)
cr1 = CartPosition.objects.get(id=cr1.id)
- self.assertIsNone(cr1.attendee_name)
+ assert not cr1.attendee_name
def test_invoice_address_required(self):
self.event.settings.invoice_address_asked = True
self.event.settings.invoice_address_required = True
+ self.event.settings.set('name_scheme', 'title_given_middle_family')
CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
@@ -609,7 +646,10 @@ class CheckoutTestCase(TestCase):
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'is_business': 'business',
'company': 'Foo',
- 'name': 'Bar',
+ 'name_parts_0': 'Mr',
+ 'name_parts_1': 'John',
+ 'name_parts_2': '',
+ 'name_parts_3': 'Kennedy',
'street': 'Baz',
'zipcode': '12345',
'city': 'Here',
@@ -619,6 +659,15 @@ class CheckoutTestCase(TestCase):
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
target_status_code=200)
+ ia = InvoiceAddress.objects.last()
+ assert ia.name_parts == {
+ 'title': 'Mr',
+ 'given_name': 'John',
+ 'middle_name': '',
+ 'family_name': 'Kennedy',
+ "_scheme": "title_given_middle_family"
+ }
+ assert ia.name_cached == 'Mr John Kennedy'
def test_invoice_address_optional(self):
self.event.settings.invoice_address_asked = True
@@ -653,7 +702,7 @@ class CheckoutTestCase(TestCase):
)
response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.rendered_content, "lxml")
- self.assertEqual(len(doc.select('input[name=name]')), 1)
+ self.assertEqual(len(doc.select('input[name=name_parts_0]')), 1)
self.assertEqual(len(doc.select('input[name=street]')), 0)
# Not all required fields filled out, expect failure
@@ -665,7 +714,7 @@ class CheckoutTestCase(TestCase):
# Corrected request
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
- 'name': 'Raphael',
+ 'name_parts_0': 'Raphael',
'email': 'admin@localhost'
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
@@ -762,7 +811,7 @@ class CheckoutTestCase(TestCase):
self.event.settings.set('invoice_address_required', True)
ia = InvoiceAddress.objects.create(
is_business=True, vat_id='ATU1234567', vat_id_validated=True,
- country=Country('DE'), name='Foo', street='Foo'
+ country=Country('DE'), name_parts={'full_name': 'Foo', "_scheme": "full"}, name_cached='Foo', street='Foo'
)
self._set_session('invoice_address', ia.pk)
CartPosition.objects.create(
@@ -786,7 +835,7 @@ class CheckoutTestCase(TestCase):
self.event.settings.set('invoice_address_required', True)
ia = InvoiceAddress.objects.create(
is_business=True, vat_id='ATU1234567', vat_id_validated=True,
- country=Country('CH'), name='Foo', street='Foo'
+ country=Country('CH'), name_parts={'full_name': 'Foo', "_scheme": "full"}, name_cached='Foo', street='Foo'
)
self._set_session('invoice_address', ia.pk)
CartPosition.objects.create(
@@ -828,7 +877,7 @@ class CheckoutTestCase(TestCase):
self.assertRedirects(response, '/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug),
target_status_code=200)
- cr1.attendee_name = 'Peter'
+ cr1.attendee_name_parts = {"full_name": 'Peter', "_scheme": "full"}
cr1.save()
q1 = Question.objects.create(
event=self.event, question='Age', type=Question.TYPE_NUMBER,
diff --git a/src/tests/presale/test_orders.py b/src/tests/presale/test_orders.py
index 5f9e30f9ad..8070586148 100644
--- a/src/tests/presale/test_orders.py
+++ b/src/tests/presale/test_orders.py
@@ -61,7 +61,7 @@ class OrdersTest(TestCase):
item=self.ticket,
variation=None,
price=Decimal("23"),
- attendee_name="Peter"
+ attendee_name_parts={'full_name': "Peter"}
)
self.not_my_order = Order.objects.create(
status=Order.STATUS_PENDING,
@@ -147,12 +147,12 @@ class OrdersTest(TestCase):
response = self.client.get(
'/%s/%s/order/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret))
doc = BeautifulSoup(response.rendered_content, "lxml")
- self.assertEqual(len(doc.select('input[name=%s-attendee_name]' % self.ticket_pos.id)), 1)
+ self.assertEqual(len(doc.select('input[name=%s-attendee_name_parts_0]' % self.ticket_pos.id)), 1)
# Not all fields filled out, expect success
response = self.client.post(
'/%s/%s/order/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), {
- '%s-attendee_name' % self.ticket_pos.id: '',
+ '%s-attendee_name_parts_0' % self.ticket_pos.id: '',
}, follow=True)
self.assertRedirects(response,
'/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code,
@@ -168,19 +168,19 @@ class OrdersTest(TestCase):
response = self.client.get(
'/%s/%s/order/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret))
doc = BeautifulSoup(response.rendered_content, "lxml")
- self.assertEqual(len(doc.select('input[name=%s-attendee_name]' % self.ticket_pos.id)), 1)
+ self.assertEqual(len(doc.select('input[name=%s-attendee_name_parts_0]' % self.ticket_pos.id)), 1)
# Not all required fields filled out, expect failure
response = self.client.post(
'/%s/%s/order/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), {
- '%s-attendee_name' % self.ticket_pos.id: '',
+ '%s-attendee_name_parts_0' % self.ticket_pos.id: '',
}, follow=True)
doc = BeautifulSoup(response.rendered_content, "lxml")
self.assertGreaterEqual(len(doc.select('.has-error')), 1)
response = self.client.post(
'/%s/%s/order/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), {
- '%s-attendee_name' % self.ticket_pos.id: 'Peter',
+ '%s-attendee_name_parts_0' % self.ticket_pos.id: 'Peter',
}, follow=True)
self.assertRedirects(response, '/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code,
self.order.secret),
diff --git a/src/tests/presale/test_widget.py b/src/tests/presale/test_widget.py
index cc3cb11e2a..47c4887f6d 100644
--- a/src/tests/presale/test_widget.py
+++ b/src/tests/presale/test_widget.py
@@ -30,7 +30,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
item=self.ticket,
variation=None,
price=Decimal("23"),
- attendee_name="Peter"
+ attendee_name_parts={'full_name': "Peter"}
)
def test_iframe_entry_view_wrapper(self):