diff --git a/.travis.sh b/.travis.sh
index 15691b539..dfdfbd91b 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 8648c01ac..bfc8ecc99 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 1adaa5341..05679e80b 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 6a207eb0a..cada3fb78 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 e16d4ad0b..f15284c20 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 816ba0fa0..ac981934f 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 dc4cfe344..fdf36b0fe 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 b88338042..fa6f006e2 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 bfc40954f..b5d8096d8 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 6a2b4303b..e0bbd13e4 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 ea4c4e951..169b45f62 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 1bb317176..2baeb642e 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 b1be284a1..846d5b73f 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 0b25256bd..a17cee959 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 d2b786c54..4422ad8b9 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 17aae79ca..3240da3ea 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 000000000..4de63daa1
--- /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 eaed7e10f..ffc0daec3 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 080196924..ed00d54b2 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 791d8735c..35bdc50b4 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 5f2bb170b..b28550bd7 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 e412690e4..cbc2a8a3e 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 39a237678..421016225 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 1092a2810..cc4ff0e0f 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 5b724153c..21242ecd5 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 2684e7f96..e105dc63f 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 0a4097075..a4581b0a7 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 6c9dfc3ac..a063b4bb3 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 d0f9508dc..8cd87afa2 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 77eda7b51..17d380d5b 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 220ce0df4..9288223d6 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 aa4c7c416..8c1539084 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 0884143a3..86638a44c 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 6f9b45a51..720069aca 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 cbe6c1e51..07765a006 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 000000000..627c8b50c
--- /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 eb8efbcb9..c8591b705 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 0d2d3b882..ee8be2dd6 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 4da62de31..99c68986c 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 92e058c48..d1931ef10 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 4b9b38451..1fd938b1a 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 018e5d8fa..4c1996bb6 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 839f6d917..cfc0949d2 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 8386f5060..7d311f260 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 e6a8e7a1a..7ab5f286a 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 deab58fa3..000000000
--- 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 897774426..d336138a0 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 6f32b74c6..44ac42271 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 52957619e..d6e2886b7 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 a156b69c2..c6cda1e55 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 d3fcb4c14..4e740d93f 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 6be5cacbc..cd37b7362 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 72c365a83..6a382c1fd 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 d45a1e4e8..a60c2fcbd 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 e892d1c63..70ecfef5e 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 e7686482b..ff27c6e7f 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 2d07aa460..a226751b2 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 9fd4c706c..b2321020d 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 dd57ecb42..931d08f70 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 5ef62e34e..c857bb8dd 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 fb5ef5174..bb7add526 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 9710d60e0..9ea88654f 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 d4da38764..a554e609f 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 9765763e2..4c9ecf84b 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 d9528b666..ad379edbe 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 9da6b425d..06964328f 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 000000000..bd5b210d0
--- /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 8d3891806..dad1c0c0e 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 0469e517d..eb4288872 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 5f9e30f9a..807058614 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 cc3cb11e2..47c4887f6 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):