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

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

View File

@@ -11,7 +11,6 @@ fi
if [ "$PRETIX_CONFIG_FILE" == "tests/travis_postgres.cfg" ]; then if [ "$PRETIX_CONFIG_FILE" == "tests/travis_postgres.cfg" ]; then
psql -c 'create database travis_ci_test;' -U postgres psql -c 'create database travis_ci_test;' -U postgres
pip3 install -Ur src/requirements/postgres.txt
fi fi
if [ "$1" == "style" ]; then if [ "$1" == "style" ]; then

View File

@@ -32,6 +32,7 @@ matrix:
env: JOB=translation-spelling env: JOB=translation-spelling
addons: addons:
postgresql: "9.4" postgresql: "9.4"
mariadb: '10.3'
apt: apt:
packages: packages:
- enchant - enchant

View File

@@ -30,7 +30,7 @@ RUN chmod +x /usr/local/bin/pretix && \
pip3 install -U pip wheel setuptools && \ pip3 install -U pip wheel setuptools && \
cd /pretix/src && \ cd /pretix/src && \
rm -f pretix.cfg && \ 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 && \ -r requirements/memcached.txt -r requirements/redis.txt gunicorn && \
mkdir -p data && \ mkdir -p data && \
chown -R pretixuser:pretixuser /pretix /data data && \ chown -R pretixuser:pretixuser /pretix /data data && \

View File

@@ -26,7 +26,7 @@ installation guides):
* `Docker`_ * `Docker`_
* A SMTP server to send out mails, e.g. `Postfix`_ on your machine or some third-party server you have credentials for * 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 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 * A `redis`_ server
We also recommend that you use a firewall, although this is not a pretix-specific recommendation. If you're new to 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 SSL certificates can be obtained for free these days. We also *do not* provide support for HTTP-only
installations except for evaluation purposes. 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 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:: our database's shell, e.g. for MySQL::
$ mysql -u root -p $ 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> GRANT ALL PRIVILEGES ON pretix.* TO pretix@'localhost' IDENTIFIED BY '*********';
mysql> FLUSH PRIVILEGES; mysql> FLUSH PRIVILEGES;

View File

@@ -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:: 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 * 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 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. faster. Also, you need a proxying web server in front to provide SSL encryption.

View File

@@ -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 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 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 * A `redis`_ server
We also recommend that you use a firewall, although this is not a pretix-specific recommendation. If you're new to 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 SSL certificates can be obtained for free these days. We also *do not* provide support for HTTP-only
installations except for evaluation purposes. 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 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:: of database managing tool or directly on our database's shell, e.g. for MySQL::
$ mysql -u root -p $ 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> GRANT ALL PRIVILEGES ON pretix.* TO pretix@'localhost' IDENTIFIED BY '*********';
mysql> FLUSH PRIVILEGES; mysql> FLUSH PRIVILEGES;

View File

@@ -25,6 +25,7 @@ item integer ID of the item
variation integer ID of the variation (or ``null``) variation integer ID of the variation (or ``null``)
price money (string) Price of this position price money (string) Price of this position
attendee_name string Specified attendee name for this position (or ``null``) 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``) 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``) 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``) 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, "variation": null,
"price": "23.00", "price": "23.00",
"attendee_name": null, "attendee_name": null,
"attendee_name_parts": {},
"attendee_email": null, "attendee_email": null,
"voucher": null, "voucher": null,
"addon_to": null, "addon_to": null,
@@ -122,6 +124,7 @@ Cart position endpoints
"variation": null, "variation": null,
"price": "23.00", "price": "23.00",
"attendee_name": null, "attendee_name": null,
"attendee_name_parts": {},
"attendee_email": null, "attendee_email": null,
"voucher": null, "voucher": null,
"addon_to": null, "addon_to": null,
@@ -175,7 +178,7 @@ Cart position endpoints
* ``item`` * ``item``
* ``variation`` (optional) * ``variation`` (optional)
* ``price`` * ``price``
* ``attendee_name`` (optional) * ``attendee_name`` **or** ``attendee_name_parts`` (optional)
* ``attendee_email`` (optional) * ``attendee_email`` (optional)
* ``subevent`` (optional) * ``subevent`` (optional)
* ``expires`` (optional) * ``expires`` (optional)
@@ -199,7 +202,10 @@ Cart position endpoints
"item": 1, "item": 1,
"variation": null, "variation": null,
"price": "23.00", "price": "23.00",
"attendee_name": "Peter", "attendee_name_parts": {
"given_name": "Peter",
"family_name": "Miller"
},
"attendee_email": null, "attendee_email": null,
"answers": [ "answers": [
{ {

View File

@@ -371,6 +371,9 @@ Order position endpoints
"variation": null, "variation": null,
"price": "23.00", "price": "23.00",
"attendee_name": "Peter", "attendee_name": "Peter",
"attendee_name_parts": {
"full_name": "Peter",
},
"attendee_email": null, "attendee_email": null,
"voucher": null, "voucher": null,
"tax_rate": "0.00", "tax_rate": "0.00",
@@ -466,6 +469,9 @@ Order position endpoints
"variation": null, "variation": null,
"price": "23.00", "price": "23.00",
"attendee_name": "Peter", "attendee_name": "Peter",
"attendee_name_parts": {
"full_name": "Peter",
},
"attendee_email": null, "attendee_email": null,
"voucher": null, "voucher": null,
"tax_rate": "0.00", "tax_rate": "0.00",

View File

@@ -46,6 +46,7 @@ invoice_address object Invoice address
for orders created before pretix 1.7, do not rely on for orders created before pretix 1.7, do not rely on
it). it).
├ name string Customer name ├ name string Customer name
├ name_parts object of strings Customer name decomposition
├ street string Customer street ├ street string Customer street
├ zipcode string Customer ZIP code ├ zipcode string Customer ZIP code
├ city string Customer city ├ city string Customer city
@@ -137,6 +138,7 @@ item integer ID of the purch
variation integer ID of the purchased variation (or ``null``) variation integer ID of the purchased variation (or ``null``)
price money (string) Price of this position price money (string) Price of this position
attendee_name string Specified attendee name for this position (or ``null``) 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``) 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``) voucher integer Internal ID of the voucher used for this position (or ``null``)
tax_rate decimal (string) VAT rate applied for this position tax_rate decimal (string) VAT rate applied for this position
@@ -278,6 +280,7 @@ List of all orders
"is_business": True, "is_business": True,
"company": "Sample company", "company": "Sample company",
"name": "John Doe", "name": "John Doe",
"name_parts": {"full_name": "John Doe"},
"street": "Test street 12", "street": "Test street 12",
"zipcode": "12345", "zipcode": "12345",
"city": "Testington", "city": "Testington",
@@ -295,6 +298,9 @@ List of all orders
"variation": null, "variation": null,
"price": "23.00", "price": "23.00",
"attendee_name": "Peter", "attendee_name": "Peter",
"attendee_name_parts": {
"full_name": "Peter",
},
"attendee_email": null, "attendee_email": null,
"voucher": null, "voucher": null,
"tax_rate": "0.00", "tax_rate": "0.00",
@@ -410,6 +416,7 @@ Fetching individual orders
"company": "Sample company", "company": "Sample company",
"is_business": True, "is_business": True,
"name": "John Doe", "name": "John Doe",
"name_parts": {"full_name": "John Doe"},
"street": "Test street 12", "street": "Test street 12",
"zipcode": "12345", "zipcode": "12345",
"city": "Testington", "city": "Testington",
@@ -427,6 +434,9 @@ Fetching individual orders
"variation": null, "variation": null,
"price": "23.00", "price": "23.00",
"attendee_name": "Peter", "attendee_name": "Peter",
"attendee_name_parts": {
"full_name": "Peter",
},
"attendee_email": null, "attendee_email": null,
"voucher": null, "voucher": null,
"tax_rate": "0.00", "tax_rate": "0.00",
@@ -601,7 +611,7 @@ Creating orders
* ``company`` * ``company``
* ``is_business`` * ``is_business``
* ``name`` * ``name`` **or** ``name_parts``
* ``street`` * ``street``
* ``zipcode`` * ``zipcode``
* ``city`` * ``city``
@@ -615,7 +625,7 @@ Creating orders
* ``item`` * ``item``
* ``variation`` * ``variation``
* ``price`` * ``price``
* ``attendee_name`` * ``attendee_name`` **or** ``attendee_name_parts``
* ``attendee_email`` * ``attendee_email``
* ``secret`` (optional) * ``secret`` (optional)
* ``addon_to`` (optional, see below) * ``addon_to`` (optional, see below)
@@ -664,7 +674,7 @@ Creating orders
"invoice_address": { "invoice_address": {
"is_business": False, "is_business": False,
"company": "Sample company", "company": "Sample company",
"name": "John Doe", "name_parts": {"full_name": "John Doe"},
"street": "Sesam Street 12", "street": "Sesam Street 12",
"zipcode": "12345", "zipcode": "12345",
"city": "Sample City", "city": "Sample City",
@@ -678,7 +688,9 @@ Creating orders
"item": 1, "item": 1,
"variation": null, "variation": null,
"price": "23.00", "price": "23.00",
"attendee_name": "Peter", "attendee_name_parts": {
"full_name": "Peter"
},
"attendee_email": null, "attendee_email": null,
"addon_to": null, "addon_to": null,
"answers": [ "answers": [
@@ -1075,6 +1087,9 @@ List of all order positions
"variation": null, "variation": null,
"price": "23.00", "price": "23.00",
"attendee_name": "Peter", "attendee_name": "Peter",
"attendee_name_parts": {
"full_name": "Peter"
},
"attendee_email": null, "attendee_email": null,
"voucher": null, "voucher": null,
"tax_rate": "0.00", "tax_rate": "0.00",
@@ -1172,6 +1187,9 @@ Fetching individual positions
"variation": null, "variation": null,
"price": "23.00", "price": "23.00",
"attendee_name": "Peter", "attendee_name": "Peter",
"attendee_name_parts": {
"full_name": "Peter",
},
"attendee_email": null, "attendee_email": null,
"voucher": null, "voucher": null,
"tax_rate": "0.00", "tax_rate": "0.00",

View File

@@ -19,18 +19,19 @@ class CartPositionSerializer(I18nAwareModelSerializer):
class Meta: class Meta:
model = CartPosition model = CartPosition
fields = ('id', 'cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_email', fields = ('id', 'cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
'voucher', 'addon_to', 'subevent', 'datetime', 'expires', 'includes_tax', 'attendee_email', 'voucher', 'addon_to', 'subevent', 'datetime', 'expires', 'includes_tax',
'answers',) 'answers',)
class CartPositionCreateSerializer(I18nAwareModelSerializer): class CartPositionCreateSerializer(I18nAwareModelSerializer):
answers = AnswerCreateSerializer(many=True, required=False) answers = AnswerCreateSerializer(many=True, required=False)
expires = serializers.DateTimeField(required=False) expires = serializers.DateTimeField(required=False)
attendee_name = serializers.CharField(required=False)
class Meta: class Meta:
model = CartPosition 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',) 'subevent', 'expires', 'includes_tax', 'answers',)
def create(self, validated_data): def create(self, validated_data):
@@ -65,6 +66,11 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
quota.name 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) cp = CartPosition.objects.create(event=self.context['event'], **validated_data)
for answ_data in answers_data: for answ_data in answers_data:
@@ -118,4 +124,8 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
raise ValidationError( raise ValidationError(
'You cannot specify a variation for this item.' '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 return data

View File

@@ -35,11 +35,12 @@ class CompatibleCountryField(serializers.Field):
class InvoiceAddressSerializer(I18nAwareModelSerializer): class InvoiceAddressSerializer(I18nAwareModelSerializer):
country = CompatibleCountryField(source='*') country = CompatibleCountryField(source='*')
name = serializers.CharField(required=False)
class Meta: class Meta:
model = InvoiceAddress model = InvoiceAddress
fields = ('last_modified', 'is_business', 'company', 'name', 'street', 'zipcode', 'city', 'country', 'vat_id', fields = ('last_modified', 'is_business', 'company', 'name', 'name_parts', 'street', 'zipcode', 'city', 'country',
'vat_id_validated', 'internal_reference') 'vat_id', 'vat_id_validated', 'internal_reference')
read_only_fields = ('last_modified', 'vat_id_validated') read_only_fields = ('last_modified', 'vat_id_validated')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@@ -48,6 +49,15 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
v.required = False v.required = False
v.allow_blank = True 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): class AnswerQuestionIdentifierField(serializers.Field):
def to_representation(self, instance: QuestionAnswer): def to_representation(self, instance: QuestionAnswer):
@@ -158,9 +168,9 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
class Meta: class Meta:
model = OrderPosition model = OrderPosition
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_email', fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins', 'downloads', 'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data') 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@@ -305,10 +315,11 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
answers = AnswerCreateSerializer(many=True, required=False) answers = AnswerCreateSerializer(many=True, required=False)
addon_to = serializers.IntegerField(required=False, allow_null=True) addon_to = serializers.IntegerField(required=False, allow_null=True)
secret = serializers.CharField(required=False) secret = serializers.CharField(required=False)
attendee_name = serializers.CharField(required=False)
class Meta: class Meta:
model = OrderPosition 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') 'secret', 'addon_to', 'subevent', 'answers')
def validate_secret(self, secret): def validate_secret(self, secret):
@@ -359,6 +370,12 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
raise ValidationError( raise ValidationError(
{'variation': ['You cannot specify a variation for this item.']} {'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 return data
@@ -464,7 +481,13 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
payment_info = validated_data.pop('payment_info', '{}') payment_info = validated_data.pop('payment_info', '{}')
if 'invoice_address' in validated_data: 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: else:
ia = None ia = None
@@ -555,6 +578,11 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
for pos_data in positions_data: for pos_data in positions_data:
answers_data = pos_data.pop('answers', []) answers_data = pos_data.pop('answers', [])
addon_to = pos_data.pop('addon_to', None) 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 = OrderPosition(**pos_data)
pos.order = order pos.order = order
pos._calculate_tax() pos._calculate_tax()

View File

@@ -154,7 +154,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = OrderPositionSerializer serializer_class = OrderPositionSerializer
queryset = OrderPosition.objects.none() queryset = OrderPosition.objects.none()
filter_backends = (DjangoFilterBackend, RichOrderingFilter) filter_backends = (DjangoFilterBackend, RichOrderingFilter)
ordering = ('attendee_name', 'positionid') ordering = ('attendee_name_cached', 'positionid')
ordering_fields = ( ordering_fields = (
'order__code', 'order__datetime', 'positionid', 'attendee_name', 'order__code', 'order__datetime', 'positionid', 'attendee_name',
'last_checked_in', 'order__email', 'last_checked_in', 'order__email',
@@ -162,11 +162,11 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
ordering_custom = { ordering_custom = {
'attendee_name': { 'attendee_name': {
'_order': F('display_name').asc(nulls_first=True), '_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': { '-attendee_name': {
'_order': F('display_name').desc(nulls_last=True), '_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': { 'last_checked_in': {
'_order': FixedOrderBy(F('last_checked_in'), nulls_first=True), '_order': FixedOrderBy(F('last_checked_in'), nulls_first=True),

View File

@@ -3,8 +3,8 @@ import datetime
import django_filters import django_filters
import pytz import pytz
from django.db import transaction from django.db import transaction
from django.db.models import Prefetch, Q from django.db.models import F, Prefetch, Q
from django.db.models.functions import Concat from django.db.models.functions import Coalesce, Concat
from django.http import FileResponse from django.http import FileResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils.timezone import make_aware, now from django.utils.timezone import make_aware, now
@@ -373,17 +373,17 @@ class OrderPositionFilter(FilterSet):
def search_qs(self, queryset, name, value): def search_qs(self, queryset, name, value):
return queryset.filter( return queryset.filter(
Q(secret__istartswith=value) Q(secret__istartswith=value)
| Q(attendee_name__icontains=value) | Q(attendee_name_cached__icontains=value)
| Q(addon_to__attendee_name__icontains=value) | Q(addon_to__attendee_name_cached__icontains=value)
| Q(order__code__istartswith=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): def has_checkin_qs(self, queryset, name, value):
return queryset.filter(checkins__isnull=not value) return queryset.filter(checkins__isnull=not value)
def attendee_name_qs(self, queryset, name, 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: class Meta:
model = OrderPosition model = OrderPosition
@@ -409,6 +409,16 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
filterset_class = OrderPositionFilter filterset_class = OrderPositionFilter
permission = 'can_view_orders' permission = 'can_view_orders'
write_permission = 'can_change_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): def get_queryset(self):
return OrderPosition.objects.filter(order__event=self.request.event).prefetch_related( return OrderPosition.objects.filter(order__event=self.request.event).prefetch_related(

View File

@@ -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 import InvoiceAddress, Order, OrderPosition
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
from pretix.base.settings import PERSON_NAME_SCHEMES
from ..exporter import BaseExporter from ..exporter import BaseExporter
from ..signals import register_data_exporters from ..signals import register_data_exporters
@@ -74,7 +75,14 @@ class OrderListExporter(BaseExporter):
headers = [ headers = [
_('Order code'), _('Order total'), _('Status'), _('Email'), _('Order date'), _('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') _('Date of last payment'), _('Fees'), _('Order locale')
] ]
@@ -118,6 +126,13 @@ class OrderListExporter(BaseExporter):
row += [ row += [
order.invoice_address.company, order.invoice_address.company,
order.invoice_address.name, 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.street,
order.invoice_address.zipcode, order.invoice_address.zipcode,
order.invoice_address.city, order.invoice_address.city,
@@ -126,7 +141,7 @@ class OrderListExporter(BaseExporter):
order.invoice_address.vat_id, order.invoice_address.vat_id,
] ]
except InvoiceAddress.DoesNotExist: except InvoiceAddress.DoesNotExist:
row += ['', '', '', '', '', '', ''] row += [''] * 7 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0)
row += [ row += [
order.payment_date.astimezone(tz).strftime('%Y-%m-%d') if order.payment_date else '', order.payment_date.astimezone(tz).strftime('%Y-%m-%d') if order.payment_date else '',

View File

@@ -1,3 +1,4 @@
import copy
import logging import logging
from decimal import Decimal from decimal import Decimal
@@ -8,6 +9,7 @@ import vat_moss.id
from django import forms from django import forms
from django.contrib import messages from django.contrib import messages
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from pretix.base.forms.widgets import ( 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 import InvoiceAddress, Question
from pretix.base.models.tax import EU_COUNTRIES from pretix.base.models.tax import EU_COUNTRIES
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.control.forms import SplitDateTimeField from pretix.control.forms import SplitDateTimeField
from pretix.helpers.i18n import get_format_without_seconds from pretix.helpers.i18n import get_format_without_seconds
from pretix.presale.signals import question_form_fields from pretix.presale.signals import question_form_fields
@@ -23,6 +26,103 @@ from pretix.presale.signals import question_form_fields
logger = logging.getLogger(__name__) 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 '<div class="nameparts-form-group">%s</div>' % ''.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): class BaseQuestionsForm(forms.Form):
""" """
This form class is responsible for asking order-related questions. This includes This form class is responsible for asking order-related questions. This includes
@@ -47,10 +147,12 @@ class BaseQuestionsForm(forms.Form):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if item.admission and event.settings.attendee_names_asked: if item.admission and event.settings.attendee_names_asked:
self.fields['attendee_name'] = forms.CharField( self.fields['attendee_name_parts'] = NamePartsFormField(
max_length=255, required=event.settings.attendee_names_required, max_length=255,
required=event.settings.attendee_names_required,
scheme=event.settings.name_scheme,
label=_('Attendee name'), 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: if item.admission and event.settings.attendee_emails_asked:
self.fields['attendee_email'] = forms.EmailField( self.fields['attendee_email'] = forms.EmailField(
@@ -170,13 +272,12 @@ class BaseInvoiceAddressForm(forms.ModelForm):
class Meta: class Meta:
model = InvoiceAddress 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') 'internal_reference')
widgets = { widgets = {
'is_business': BusinessBooleanRadio, 'is_business': BusinessBooleanRadio,
'street': forms.Textarea(attrs={'rows': 2, 'placeholder': _('Street and Number')}), 'street': forms.Textarea(attrs={'rows': 2, 'placeholder': _('Street and Number')}),
'company': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}), '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'}), 'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}),
'internal_reference': forms.TextInput, 'internal_reference': forms.TextInput,
} }
@@ -191,15 +292,13 @@ class BaseInvoiceAddressForm(forms.ModelForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if not event.settings.invoice_address_vatid: if not event.settings.invoice_address_vatid:
del self.fields['vat_id'] del self.fields['vat_id']
if not event.settings.invoice_address_required: if not event.settings.invoice_address_required:
for k, f in self.fields.items(): for k, f in self.fields.items():
f.required = False f.required = False
f.widget.is_required = False f.widget.is_required = False
if 'required' in f.widget.attrs: if 'required' in f.widget.attrs:
del f.widget.attrs['required'] del f.widget.attrs['required']
if event.settings.invoice_name_required:
self.fields['name'].required = True
elif event.settings.invoice_address_company_required: elif event.settings.invoice_address_company_required:
self.initial['is_business'] = True self.initial['is_business'] = True
@@ -210,20 +309,34 @@ class BaseInvoiceAddressForm(forms.ModelForm):
del self.fields['company'].widget.attrs['data-display-dependency'] del self.fields['company'].widget.attrs['data-display-dependency']
if 'vat_id' in self.fields: if 'vat_id' in self.fields:
del self.fields['vat_id'].widget.attrs['data-display-dependency'] 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['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): def clean(self):
data = self.cleaned_data data = self.cleaned_data
if not data.get('is_business'): if not data.get('is_business'):
data['company'] = '' data['company'] = ''
if not data.get('name') and not data.get('company') and self.event.settings.invoice_address_required: if self.event.settings.invoice_address_required:
raise ValidationError(_('You need to provide either a company name or your name.')) 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'): if 'vat_id' in self.changed_data or not data.get('vat_id'):
self.instance.vat_id_validated = False 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: if self.validate_vat_id and self.instance.vat_id_validated and 'vat_id' not in self.changed_data:
pass pass
elif self.validate_vat_id and data.get('is_business') and data.get('country') in EU_COUNTRIES and data.get('vat_id'): elif self.validate_vat_id and data.get('is_business') and data.get('country') in EU_COUNTRIES and data.get('vat_id'):

View File

@@ -28,7 +28,8 @@ class Migration(migrations.Migration):
('password', models.CharField(verbose_name='password', max_length=128)), ('password', models.CharField(verbose_name='password', max_length=128)),
('last_login', models.DateTimeField(verbose_name='last login', blank=True, null=True)), ('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.')), ('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)), ('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)), ('familyname', models.CharField(verbose_name='Family name', max_length=255, blank=True, null=True)),
('is_active', models.BooleanField(verbose_name='Is active', default=True)), ('is_active', models.BooleanField(verbose_name='Is active', default=True)),

View File

@@ -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)
]

View File

@@ -75,7 +75,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
REQUIRED_FIELDS = [] REQUIRED_FIELDS = []
email = models.EmailField(unique=True, db_index=True, null=True, blank=True, 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, fullname = models.CharField(max_length=255, blank=True, null=True,
verbose_name=_('Full name')) verbose_name=_('Full name'))
is_active = models.BooleanField(default=True, is_active = models.BooleanField(default=True,

View File

@@ -26,10 +26,12 @@ from django.utils.timezone import make_aware, now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _ from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django_countries.fields import CountryField from django_countries.fields import CountryField
from i18nfield.strings import LazyI18nString from i18nfield.strings import LazyI18nString
from jsonfallback.fields import FallbackJSONField
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.models import User from pretix.base.models import User
from pretix.base.reldate import RelativeDateWrapper from pretix.base.reldate import RelativeDateWrapper
from pretix.base.settings import PERSON_NAME_SCHEMES
from .base import LockModel, LoggedModel from .base import LockModel, LoggedModel
from .event import Event, SubEvent from .event import Event, SubEvent
@@ -699,8 +701,10 @@ class AbstractPosition(models.Model):
:type expires: datetime :type expires: datetime
:param price: The price of this item :param price: The price of this item
:type price: decimal.Decimal :type price: decimal.Decimal
:param attendee_name: The attendee's name, if entered. :param attendee_name_parts: The parts of the attendee's name, if entered.
:type attendee_name: str :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. :param attendee_email: The attendee's email, if entered.
:type attendee_email: str :type attendee_email: str
:param voucher: A voucher that has been applied to this sale :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, decimal_places=2, max_digits=10,
verbose_name=_("Price") verbose_name=_("Price")
) )
attendee_name = models.CharField( attendee_name_cached = models.CharField(
max_length=255, max_length=255,
verbose_name=_("Attendee name"), verbose_name=_("Attendee name"),
blank=True, null=True, blank=True, null=True,
help_text=_("Empty, if this product is not an admission ticket") help_text=_("Empty, if this product is not an admission ticket")
) )
attendee_name_parts = FallbackJSONField(
blank=True, default=dict
)
attendee_email = models.EmailField( attendee_email = models.EmailField(
verbose_name=_("Attendee email"), verbose_name=_("Attendee email"),
blank=True, null=True, blank=True, null=True,
@@ -797,6 +804,24 @@ class AbstractPosition(models.Model):
if self.variation is None if self.variation is None
else self.variation.quotas.filter(subevent=self.subevent)) 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): class OrderPayment(models.Model):
""" """
@@ -1482,6 +1507,10 @@ class OrderPosition(AbstractPosition):
self.pseudonymization_id = code self.pseudonymization_id = code
return return
@property
def event(self):
return self.order.event
class CartPosition(AbstractPosition): 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) 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')) is_business = models.BooleanField(default=False, verbose_name=_('Business customer'))
company = models.CharField(max_length=255, blank=True, verbose_name=_('Company name')) 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) street = models.TextField(verbose_name=_('Address'), blank=False)
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), 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) city = models.CharField(max_length=255, verbose_name=_('City'), blank=False)
@@ -1565,8 +1595,25 @@ class InvoiceAddress(models.Model):
def save(self, **kwargs): def save(self, **kwargs):
if self.order: if self.order:
self.order.touch() self.order.touch()
if self.name_parts:
self.name_cached = self.name
else:
self.name_cached = ""
super().save(**kwargs) 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: def cachedticket_name(instance, filename: str) -> str:
secret = get_random_string(length=16, allowed_chars=string.ascii_letters + string.digits) secret = get_random_string(length=16, allowed_chars=string.ascii_letters + string.digits)

View File

@@ -26,6 +26,7 @@ from reportlab.platypus import Paragraph
from pretix.base.invoice import ThumbnailingImageReader from pretix.base.invoice import ThumbnailingImageReader
from pretix.base.models import Order, OrderPosition 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.signals import layout_text_variables
from pretix.base.templatetags.money import money_filter from pretix.base.templatetags.money import money_filter
from pretix.presale.style import get_fonts from pretix.presale.style import get_fonts
@@ -147,12 +148,12 @@ DEFAULT_VARIABLES = OrderedDict((
"evaluate": lambda op, order, ev: str(ev.location).replace("\n", "<br/>\n") "evaluate": lambda op, order, ev: str(ev.location).replace("\n", "<br/>\n")
}), }),
("invoice_name", { ("invoice_name", {
"label": _("Invoice address: name"), "label": _("Invoice address name"),
"editor_sample": _("John Doe"), "editor_sample": _("John Doe"),
"evaluate": lambda op, order, ev: order.invoice_address.name if getattr(order, 'invoice_address', None) else '' "evaluate": lambda op, order, ev: order.invoice_address.name if getattr(order, 'invoice_address', None) else ''
}), }),
("invoice_company", { ("invoice_company", {
"label": _("Invoice address: company"), "label": _("Invoice address company"),
"editor_sample": _("Sample company"), "editor_sample": _("Sample company"),
"evaluate": lambda op, order, ev: order.invoice_address.company if getattr(order, 'invoice_address', None) else '' "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): def get_variables(event):
v = copy.copy(DEFAULT_VARIABLES) 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): for recv, res in layout_text_variables.send(sender=event):
v.update(res) v.update(res)
return v return v

View File

@@ -12,6 +12,7 @@ from pretix.base.models import (
OrderPosition, OrderPosition,
) )
from pretix.base.services.tasks import ProfiledTask 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.base.signals import allow_ticket_download, register_ticket_outputs
from pretix.celery_app import app from pretix.celery_app import app
from pretix.helpers.database import rolledback_transaction from pretix.helpers.database import rolledback_transaction
@@ -87,11 +88,13 @@ def preview(event: int, provider: str):
locale=event.settings.locale, locale=event.settings.locale,
expires=now(), code="PREVIEW1234", total=119) expires=now(), code="PREVIEW1234", total=119)
p = order.positions.create(item=item, attendee_name=_("John Doe"), price=item.default_price) scheme = PERSON_NAME_SCHEMES[event.settings.name_scheme]
order.positions.create(item=item2, attendee_name=_("John Doe"), price=item.default_price, addon_to=p) sample = {k: str(v) for k, v in scheme['sample'].items()}
order.positions.create(item=item2, attendee_name=_("John Doe"), price=item.default_price, addon_to=p) 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) responses = register_ticket_outputs.send(event)
for receiver, response in responses: for receiver, response in responses:

View File

@@ -1,11 +1,14 @@
import json import json
from collections import OrderedDict
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
from django.conf import settings from django.conf import settings
from django.core.files import File from django.core.files import File
from django.db.models import Model 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 hierarkey.models import GlobalSettingsBase, Hierarkey
from i18nfield.strings import LazyI18nString from i18nfield.strings import LazyI18nString
@@ -556,7 +559,144 @@ Your {event} team"""))
'default': 'date_ascending', 'default': 'date_ascending',
'type': str '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') settings_hierarkey = Hierarkey(attribute_name='settings')

View File

@@ -3,7 +3,7 @@ from datetime import timedelta
from typing import List, Tuple from typing import List, Tuple
from django.db import transaction 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.db.models.functions import Greatest
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.timezone import now from django.utils.timezone import now
@@ -202,12 +202,20 @@ class AttendeeNameShredder(BaseDataShredder):
def generate_files(self) -> List[Tuple[str, str, str]]: def generate_files(self) -> List[Tuple[str, str, str]]:
yield 'attendee-names.json', 'application/json', json.dumps({ yield 'attendee-names.json', 'application/json', json.dumps({
'{}-{}'.format(op.order.code, op.positionid): op.attendee_name '{}-{}'.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) }, indent=4)
@transaction.atomic @transaction.atomic
def shred_data(self): 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=""): for le in self.event.logentry_set.filter(action_type="pretix.event.order.modified").exclude(data=""):
d = le.parsed_data d = le.parsed_data
@@ -215,6 +223,10 @@ class AttendeeNameShredder(BaseDataShredder):
for i, row in enumerate(d['data']): for i, row in enumerate(d['data']):
if 'attendee_name' in row: if 'attendee_name' in row:
d['data'][i]['attendee_name'] = '' d['data'][i]['attendee_name'] = ''
if 'attendee_name_parts' in row:
d['data'][i]['attendee_name_parts'] = {
'_legacy': ''
}
le.data = json.dumps(d) le.data = json.dumps(d)
le.shredded = True le.shredded = True
le.save(update_fields=['data', 'shredded']) le.save(update_fields=['data', 'shredded'])

View File

@@ -80,8 +80,8 @@ class BaseQuestionsViewMixin:
# This form was correctly filled, so we store the data as # This form was correctly filled, so we store the data as
# answers to the questions / in the CartPosition object # answers to the questions / in the CartPosition object
for k, v in form.cleaned_data.items(): for k, v in form.cleaned_data.items():
if k == 'attendee_name': if k == 'attendee_name_parts':
form.pos.attendee_name = v if v != '' else None form.pos.attendee_name_parts = v if v else None
form.pos.save() form.pos.save()
elif k == 'attendee_email': elif k == 'attendee_email':
form.pos.attendee_email = v if v != '' else None form.pos.attendee_email = v if v != '' else None

View File

@@ -20,6 +20,7 @@ from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
from pretix.base.models import Event, Organizer, TaxRule from pretix.base.models import Event, Organizer, TaxRule
from pretix.base.models.event import EventMetaValue, SubEvent from pretix.base.models.event import EventMetaValue, SubEvent
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.control.forms import ( from pretix.control.forms import (
ExtFileField, MultipleLanguagesWidget, SingleLanguageWidget, SlugWidget, ExtFileField, MultipleLanguagesWidget, SingleLanguageWidget, SlugWidget,
SplitDateTimeField, SplitDateTimePickerWidget, SplitDateTimeField, SplitDateTimePickerWidget,
@@ -338,6 +339,12 @@ class EventSettingsForm(SettingsForm):
required=False, required=False,
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_names_asked'}), 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( attendee_emails_asked = forms.BooleanField(
label=_("Ask for email addresses per ticket"), 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 " 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 ' 'e.g. I hereby confirm that I have read and agree with the event organizer\'s terms of service '
'and agree with them.' '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): class PaymentSettingsForm(SettingsForm):

View File

@@ -129,7 +129,7 @@ class OrderFilterForm(FilterForm):
matching_positions = OrderPosition.objects.filter( matching_positions = OrderPosition.objects.filter(
Q(order=OuterRef('pk')) & Q( 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) | Q(secret__istartswith=u)
) )
).values('id') ).values('id')
@@ -137,7 +137,7 @@ class OrderFilterForm(FilterForm):
qs = qs.annotate(has_pos=Exists(matching_positions)).filter( qs = qs.annotate(has_pos=Exists(matching_positions)).filter(
code code
| Q(email__icontains=u) | Q(email__icontains=u)
| Q(invoice_address__name__icontains=u) | Q(invoice_address__name_cached__icontains=u)
| Q(invoice_address__company__icontains=u) | Q(invoice_address__company__icontains=u)
| Q(pk__in=matching_invoices) | Q(pk__in=matching_invoices)
| Q(comment__icontains=u) | 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'),
'-item': ('-item__name', '-variation__value', '-order__code'), '-item': ('-item__name', '-variation__value', '-order__code'),
'name': {'_order': F('display_name').asc(nulls_first=True), '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), '-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( user = forms.CharField(
@@ -615,10 +615,10 @@ class CheckInFilterForm(FilterForm):
Q(order__code__istartswith=u) Q(order__code__istartswith=u)
| Q(secret__istartswith=u) | Q(secret__istartswith=u)
| Q(order__email__icontains=u) | Q(order__email__icontains=u)
| Q(attendee_name__icontains=u) | Q(attendee_name_cached__icontains=u)
| Q(attendee_email__icontains=u) | Q(attendee_email__icontains=u)
| Q(voucher__code__istartswith=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) | Q(order__invoice_address__company__icontains=u)
) )

View File

@@ -63,6 +63,7 @@
{% bootstrap_field sform.max_items_per_order layout="control" %} {% bootstrap_field sform.max_items_per_order layout="control" %}
{% bootstrap_field sform.attendee_names_asked layout="control" %} {% bootstrap_field sform.attendee_names_asked layout="control" %}
{% bootstrap_field sform.attendee_names_required 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.order_email_asked_twice layout="control" %}
{% bootstrap_field sform.attendee_emails_asked layout="control" %} {% bootstrap_field sform.attendee_emails_asked layout="control" %}
{% bootstrap_field sform.attendee_emails_required layout="control" %} {% bootstrap_field sform.attendee_emails_required layout="control" %}

View File

@@ -644,7 +644,7 @@ class MailSettingsRendererPreview(MailSettingsPreview):
expires=now(), code="PREVIEW", total=119) expires=now(), code="PREVIEW", total=119)
item = request.event.items.create(name=ugettext("Sample product"), default_price=42.23, item = request.event.items.create(name=ugettext("Sample product"), default_price=42.23,
description=ugettext("Sample product description")) 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 = renderers[request.GET.get('renderer')].render(
v, v,
str(request.event.settings.mail_text_signature), str(request.event.settings.mail_text_signature),

View File

@@ -18,6 +18,7 @@ from django.views.generic import TemplateView
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.models import CachedFile, InvoiceAddress, OrderPosition from pretix.base.models import CachedFile, InvoiceAddress, OrderPosition
from pretix.base.pdf import get_variables from pretix.base.pdf import get_variables
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.control.permissions import EventPermissionRequiredMixin from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.helpers.database import rolledback_transaction from pretix.helpers.database import rolledback_transaction
from pretix.presale.style import get_fonts from pretix.presale.style import get_fonts
@@ -65,11 +66,13 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
locale=self.request.event.settings.locale, locale=self.request.event.settings.locale,
expires=now(), code="PREVIEW1234", total=119) expires=now(), code="PREVIEW1234", total=119)
p = order.positions.create(item=item, attendee_name=_("John Doe"), price=item.default_price) scheme = PERSON_NAME_SCHEMES[self.request.event.settings.name_scheme]
order.positions.create(item=item2, attendee_name=_("John Doe"), price=item.default_price, addon_to=p) sample = {k: str(v) for k, v in scheme['sample'].items()}
order.positions.create(item=item2, attendee_name=_("John Doe"), price=item.default_price, addon_to=p) 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 return p
def generate(self, p: OrderPosition, override_layout=None, override_background=None): def generate(self, p: OrderPosition, override_layout=None, override_background=None):

View File

@@ -36,7 +36,8 @@ class OrderSearch(PaginationMixin, ListView):
qs = self.filter_form.filter_qs(qs) qs = self.filter_form.filter_qs(qs)
return qs.only( 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( ).prefetch_related(
'event', 'event__organizer' 'event', 'event__organizer'
) )

View File

@@ -4,11 +4,14 @@ from io import BytesIO
from typing import Tuple from typing import Tuple
from django import forms from django import forms
from django.conf import settings
from django.contrib.staticfiles import finders from django.contrib.staticfiles import finders
from django.core.files import File from django.core.files import File
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
from django.db.models.functions import Coalesce
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from jsonfallback.functions import JSONExtract
from PyPDF2 import PdfFileMerger from PyPDF2 import PdfFileMerger
from reportlab.lib import pagesizes from reportlab.lib import pagesizes
from reportlab.pdfgen import canvas from reportlab.pdfgen import canvas
@@ -17,6 +20,7 @@ from pretix.base.exporter import BaseExporter
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.models import Order, OrderPosition from pretix.base.models import Order, OrderPosition
from pretix.base.pdf import Renderer from pretix.base.pdf import Renderer
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.plugins.badges.models import BadgeItem, BadgeLayout from pretix.plugins.badges.models import BadgeItem, BadgeLayout
@@ -67,6 +71,7 @@ class BadgeExporter(BaseExporter):
@property @property
def export_form_fields(self): def export_form_fields(self):
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
d = OrderedDict( d = OrderedDict(
[ [
('items', ('items',
@@ -86,10 +91,13 @@ class BadgeExporter(BaseExporter):
('order_by', ('order_by',
forms.ChoiceField( forms.ChoiceField(
label=_('Sort by'), label=_('Sort by'),
choices=( choices=[
('name', _('Attendee name')), ('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]) qs = qs.filter(order__status__in=[Order.STATUS_PAID])
if form_data.get('order_by') == 'name': if form_data.get('order_by') == 'name':
qs = qs.order_by('attendee_name', 'order__code') qs = qs.order_by('attendee_name_cached', 'order__code')
elif form_data.get('order_by') == 'last_name': elif form_data.get('order_by') == 'code':
qs = qs.order_by('order__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) outbuffer = render_pdf(self.event, qs)
return 'badges.pdf', 'application/pdf', outbuffer.read() return 'badges.pdf', 'application/pdf', outbuffer.read()

View File

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

View File

@@ -4,18 +4,21 @@ from collections import OrderedDict
import dateutil.parser import dateutil.parser
from defusedcsv import csv from defusedcsv import csv
from django import forms from django import forms
from django.conf import settings
from django.db.models import Max, OuterRef, Subquery from django.db.models import Max, OuterRef, Subquery
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.urls import reverse from django.urls import reverse
from django.utils.formats import date_format from django.utils.formats import date_format
from django.utils.timezone import is_aware, make_aware from django.utils.timezone import is_aware, make_aware
from django.utils.translation import pgettext, ugettext as _, ugettext_lazy from django.utils.translation import pgettext, ugettext as _, ugettext_lazy
from jsonfallback.functions import JSONExtract
from pytz import UTC from pytz import UTC
from reportlab.lib.units import mm from reportlab.lib.units import mm
from reportlab.platypus import Flowable, Paragraph, Spacer, Table, TableStyle from reportlab.platypus import Flowable, Paragraph, Spacer, Table, TableStyle
from pretix.base.exporter import BaseExporter from pretix.base.exporter import BaseExporter
from pretix.base.models import Checkin, Order, OrderPosition, Question 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.base.templatetags.money import money_filter
from pretix.control.forms.widgets import Select2 from pretix.control.forms.widgets import Select2
from pretix.plugins.reports.exporters import ReportlabExportMixin from pretix.plugins.reports.exporters import ReportlabExportMixin
@@ -24,6 +27,7 @@ from pretix.plugins.reports.exporters import ReportlabExportMixin
class BaseCheckinList(BaseExporter): class BaseCheckinList(BaseExporter):
@property @property
def export_form_fields(self): def export_form_fields(self):
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
d = OrderedDict( d = OrderedDict(
[ [
('list', ('list',
@@ -44,10 +48,13 @@ class BaseCheckinList(BaseExporter):
forms.ChoiceField( forms.ChoiceField(
label=_('Sort by'), label=_('Sort by'),
initial='name', initial='name',
choices=( choices=[
('name', _('Attendee name')), ('name', _('Attendee name')),
('code', _('Order code')), ('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, widget=forms.RadioSelect,
required=False required=False
)), )),
@@ -79,6 +86,49 @@ class BaseCheckinList(BaseExporter):
return d 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): class CBFlowable(Flowable):
def __init__(self, checked=False): def __init__(self, checked=False):
@@ -174,37 +224,7 @@ class PDFCheckinList(ReportlabExportMixin, BaseCheckinList):
p = Paragraph(txt, headrowstyle) p = Paragraph(txt, headrowstyle)
tdata[0].append(p) tdata[0].append(p)
cqs = Checkin.objects.filter( qs = self._get_queryset(cl, form_data)
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))
for op in qs: for op in qs:
try: 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 name = op.attendee_name or (op.addon_to.attendee_name if op.addon_to else '') or ian
if iac: if iac:
name += "\n" + iac name += "<br/>" + iac
row = [ row = [
'!!' if op.item.checkin_attention else '', '!!' 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'])) questions = list(Question.objects.filter(event=self.event, id__in=form_data['questions']))
cqs = Checkin.objects.filter( qs = self._get_queryset(cl, form_data)
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')
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
headers = [ 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: if not cl.include_pending:
qs = qs.filter(order__status=Order.STATUS_PAID) qs = qs.filter(order__status=Order.STATUS_PAID)
@@ -340,6 +345,13 @@ class CSVCheckinList(BaseCheckinList):
row = [ row = [
op.order.code, op.order.code,
op.attendee_name or (op.addon_to.attendee_name if op.addon_to else ''), 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 ""), str(op.item) + (" " + str(op.variation.value) if op.variation else ""),
op.price, op.price,
date_format(last_checked_in.astimezone(self.event.timezone), 'SHORT_DATETIME_FORMAT') date_format(last_checked_in.astimezone(self.event.timezone), 'SHORT_DATETIME_FORMAT')

View File

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

View File

@@ -1,27 +1,79 @@
from collections import OrderedDict
from io import BytesIO from io import BytesIO
from django import forms
from django.conf import settings
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.db.models.functions import Coalesce
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from jsonfallback.functions import JSONExtract
from PyPDF2.merger import PdfFileMerger from PyPDF2.merger import PdfFileMerger
from pretix.base.exporter import BaseExporter from pretix.base.exporter import BaseExporter
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.models import Order, OrderPosition from pretix.base.models import Order, OrderPosition
from pretix.base.settings import PERSON_NAME_SCHEMES
from .ticketoutput import PdfTicketOutput from .ticketoutput import PdfTicketOutput
class AllTicketsPDF(BaseExporter): class AllTicketsPDF(BaseExporter):
name = "alltickets" name = "alltickets"
verbose_name = _("All paid PDF tickets in one file") verbose_name = _("All PDF tickets in one file")
identifier = "pdfoutput_all_tickets" 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): def render(self, form_data):
merger = PdfFileMerger() merger = PdfFileMerger()
o = PdfTicketOutput(self.event) o = PdfTicketOutput(self.event)
qs = OrderPosition.objects.filter(order__event=self.event, order__status=Order.STATUS_PAID).select_related( qs = OrderPosition.objects.filter(
'order', 'item', 'variation' 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: for op in qs:

View File

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

View File

@@ -391,7 +391,7 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
messages.warning(request, _('Please fill in answers to all required questions.')) messages.warning(request, _('Please fill in answers to all required questions.'))
return False return False
if cp.item.admission and self.request.event.settings.get('attendee_names_required', as_type=bool) \ 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: if warn:
messages.warning(request, _('Please fill in answers to all required questions.')) messages.warning(request, _('Please fill in answers to all required questions.'))
return False return False

View File

@@ -66,7 +66,7 @@ class InvoiceNameForm(InvoiceAddressForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
for f in list(self.fields.keys()): for f in list(self.fields.keys()):
if f != 'name': if f != 'name_parts':
del self.fields[f] del self.fields[f]

View File

@@ -34,7 +34,8 @@
{% endcompress %} {% endcompress %}
<meta name="referrer" content="origin"> <meta name="referrer" content="origin">
{{ html_head|safe }} {{ html_head|safe }}
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=0"> <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0">
<link rel="icon" href="{% static "pretixbase/img/favicon.ico" %}">
{% block custom_header %}{% endblock %} {% block custom_header %}{% endblock %}
<link rel="shortcut icon" href="{% static "pretixbase/img/favicon.ico" %}"> <link rel="shortcut icon" href="{% static "pretixbase/img/favicon.ico" %}">
<link rel="apple-touch-icon" sizes="180x180" href="{% static "pretixbase/img/icons/apple-touch-icon.png" %}"> <link rel="apple-touch-icon" sizes="180x180" href="{% static "pretixbase/img/icons/apple-touch-icon.png" %}">

View File

@@ -102,7 +102,7 @@
{% if frontpage_text and not cart_namespace %} {% if frontpage_text and not cart_namespace %}
<div> <div>
{{ frontpage_text|rich_text }} {{ frontpage_text|rich_text|linebreaksbr }}
</div> </div>
{% endif %} {% endif %}

View File

@@ -184,6 +184,8 @@ def get_cart(request):
'item', 'variation', 'subevent', 'subevent__event', 'subevent__event__organizer', 'item', 'variation', 'subevent', 'subevent__event', 'subevent__event__organizer',
'item__tax_rule' '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 return request._cart_cache

View File

@@ -58,6 +58,8 @@ debug_fallback = "runserver" in sys.argv
DEBUG = config.getboolean('django', 'debug', fallback=debug_fallback) DEBUG = config.getboolean('django', 'debug', fallback=debug_fallback)
db_backend = config.get('database', 'backend', fallback='sqlite3') 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) DATABASE_IS_GALERA = config.getboolean('database', 'galera', fallback=False)
if DATABASE_IS_GALERA and 'mysql' in db_backend: if DATABASE_IS_GALERA and 'mysql' in db_backend:
db_options = { db_options = {
@@ -66,6 +68,10 @@ if DATABASE_IS_GALERA and 'mysql' in db_backend:
else: else:
db_options = {} db_options = {}
if 'mysql' in db_backend:
db_options['charset'] = 'utf8mb4'
JSON_FIELD_AVAILABLE = db_backend in ('mysql', 'postgresql')
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.' + db_backend, 'ENGINE': 'django.db.backends.' + db_backend,
@@ -75,7 +81,11 @@ DATABASES = {
'HOST': config.get('database', 'host', fallback=''), 'HOST': config.get('database', 'host', fallback=''),
'PORT': config.get('database', 'port', fallback=''), 'PORT': config.get('database', 'port', fallback=''),
'CONN_MAX_AGE': 0 if db_backend == 'sqlite3' else 120, '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 {}
} }
} }

View File

@@ -366,3 +366,52 @@ table td > .checkbox input[type="checkbox"] {
box-shadow: 0 1px 3px 0 #aaa; 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;
}
}
}

View File

@@ -189,7 +189,10 @@ $(function () {
dependency = $($(this).attr("data-required-if")), dependency = $($(this).attr("data-required-if")),
update = function (ev) { update = function (ev) {
var enabled = (dependency.attr("type") === 'checkbox' || dependency.attr("type") === 'radio') ? dependency.prop('checked') : !!dependency.val(); 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(); update();
dependency.closest('.form-group').find('input[name=' + dependency.attr("name") + ']').on("change", update); dependency.closest('.form-group').find('input[name=' + dependency.attr("name") + ']').on("change", update);

View File

@@ -92,3 +92,52 @@
border-left: 0; 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;
}
}
}

View File

@@ -1,2 +0,0 @@
psycopg2-binary

View File

@@ -34,6 +34,8 @@ babel
django-i18nfield>=1.4.0 django-i18nfield>=1.4.0
django-hijack>=2.1.10,<2.2.0 django-hijack>=2.1.10,<2.2.0
django-oauth-toolkit==1.2.* django-oauth-toolkit==1.2.*
django-jsonfallback
psycopg2-binary
# Stripe # Stripe
stripe==2.0.* stripe==2.0.*
# PayPal # PayPal

View File

@@ -13,7 +13,7 @@ known_third_party = versions
known_standard_library = typing,enum,mimetypes known_standard_library = typing,enum,mimetypes
multi_line_output = 5 multi_line_output = 5
not_skip = __init__.py 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] [tool:pytest]
DJANGO_SETTINGS_MODULE=tests.settings DJANGO_SETTINGS_MODULE=tests.settings

View File

@@ -127,6 +127,8 @@ setup(
'chardet<3.1.0,>=3.0.2', 'chardet<3.1.0,>=3.0.2',
'mt-940==3.2', 'mt-940==3.2',
'django-i18nfield>=1.4.0', 'django-i18nfield>=1.4.0',
'django-jsonfallback',
'psycopg2-binary',
'vobject==0.9.*', 'vobject==0.9.*',
'pycountry', 'pycountry',
'django-countries', 'django-countries',
@@ -157,7 +159,6 @@ setup(
], ],
'memcached': ['pylibmc'], 'memcached': ['pylibmc'],
'mysql': ['mysqlclient'], 'mysql': ['mysqlclient'],
'postgres': ['psycopg2-binary'],
}, },
packages=find_packages(exclude=['tests', 'tests.*']), packages=find_packages(exclude=['tests', 'tests.*']),

View File

@@ -54,7 +54,8 @@ TEST_CARTPOSITION_RES = {
'item': 1, 'item': 1,
'variation': None, 'variation': None,
'price': '23.00', 'price': '23.00',
'attendee_name': None, 'attendee_name_parts': {'full_name': 'Peter'},
'attendee_name': 'Peter',
'attendee_email': None, 'attendee_email': None,
'voucher': None, 'voucher': None,
'addon_to': None, 'addon_to': None,
@@ -74,7 +75,7 @@ def test_cp_list(token_client, organizer, event, item, taxrule, question):
mock_now.return_value = testtime mock_now.return_value = testtime
cr = CartPosition.objects.create( cr = CartPosition.objects.create(
event=event, cart_id="aaa", item=item, 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), datetime=datetime.datetime(2018, 6, 11, 10, 0, 0, 0),
expires=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 mock_now.return_value = testtime
cr = CartPosition.objects.create( cr = CartPosition.objects.create(
event=event, cart_id="aaa@api", item=item, 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), datetime=datetime.datetime(2018, 6, 11, 10, 0, 0, 0),
expires=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 mock_now.return_value = testtime
cr = CartPosition.objects.create( cr = CartPosition.objects.create(
event=event, cart_id="aaa@api", item=item, 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), datetime=datetime.datetime(2018, 6, 11, 10, 0, 0, 0),
expires=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 mock_now.return_value = testtime
cr = CartPosition.objects.create( cr = CartPosition.objects.create(
event=event, cart_id="aaa@api", item=item, 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), datetime=datetime.datetime(2018, 6, 11, 10, 0, 0, 0),
expires=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, 'item': 1,
'variation': None, 'variation': None,
'price': '23.00', 'price': '23.00',
'attendee_name': None, 'attendee_name_parts': {'full_name': 'Peter'},
'attendee_email': None, 'attendee_email': None,
'addon_to': None, 'addon_to': None,
'subevent': 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']) cp = CartPosition.objects.get(pk=resp.data['id'])
assert cp.price == Decimal('23.00') assert cp.price == Decimal('23.00')
assert cp.item == item 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 @pytest.mark.django_db

View File

@@ -50,7 +50,7 @@ def order(event, item, other_item, taxrule):
item=item, item=item,
variation=None, variation=None,
price=Decimal("23"), price=Decimal("23"),
attendee_name="Peter", attendee_name_parts={'full_name': "Peter"},
secret="z3fsn8jyufm5kpk768q69gkbyr5f4h6w", secret="z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
pseudonymization_id="ABCDEFGHKL", pseudonymization_id="ABCDEFGHKL",
) )
@@ -60,7 +60,7 @@ def order(event, item, other_item, taxrule):
item=other_item, item=other_item,
variation=None, variation=None,
price=Decimal("23"), price=Decimal("23"),
attendee_name="Michael", attendee_name_parts={'full_name': "Michael"},
secret="sf4HZG73fU6kwddgjg2QOusFbYZwVKpK", secret="sf4HZG73fU6kwddgjg2QOusFbYZwVKpK",
pseudonymization_id="BACDEFGHKL", pseudonymization_id="BACDEFGHKL",
) )
@@ -75,6 +75,7 @@ TEST_ORDERPOSITION1_RES = {
"variation": None, "variation": None,
"price": "23.00", "price": "23.00",
"attendee_name": "Peter", "attendee_name": "Peter",
"attendee_name_parts": {'full_name': "Peter"},
"attendee_email": None, "attendee_email": None,
"voucher": None, "voucher": None,
"tax_rate": "0.00", "tax_rate": "0.00",
@@ -97,6 +98,7 @@ TEST_ORDERPOSITION2_RES = {
"variation": None, "variation": None,
"price": "23.00", "price": "23.00",
"attendee_name": "Michael", "attendee_name": "Michael",
"attendee_name_parts": {'full_name': "Michael"},
"attendee_email": None, "attendee_email": None,
"voucher": None, "voucher": None,
"tax_rate": "0.00", "tax_rate": "0.00",

View File

@@ -49,7 +49,7 @@ def order_position(item, order, taxrule, variations):
tax_rate=taxrule.rate, tax_rate=taxrule.rate,
tax_value=Decimal("3"), tax_value=Decimal("3"),
price=Decimal("23"), price=Decimal("23"),
attendee_name="Peter", attendee_name_parts={'full_name': "Peter"},
secret="z3fsn8jyufm5kpk768q69gkbyr5f4h6w" secret="z3fsn8jyufm5kpk768q69gkbyr5f4h6w"
) )
return op return op

View File

@@ -61,7 +61,7 @@ def order_position(item, order, taxrule, variations):
tax_rate=taxrule.rate, tax_rate=taxrule.rate,
tax_value=Decimal("3"), tax_value=Decimal("3"),
price=Decimal("23"), price=Decimal("23"),
attendee_name="Peter", attendee_name_parts={'full_name': "Peter"},
secret="z3fsn8jyufm5kpk768q69gkbyr5f4h6w" secret="z3fsn8jyufm5kpk768q69gkbyr5f4h6w"
) )
return op return op

View File

@@ -100,7 +100,7 @@ def order(event, item, taxrule, question):
item=item, item=item,
variation=None, variation=None,
price=Decimal("23"), price=Decimal("23"),
attendee_name="Peter", attendee_name_parts={"full_name": "Peter", "_scheme": "full"},
secret="z3fsn8jyufm5kpk768q69gkbyr5f4h6w", secret="z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
pseudonymization_id="ABCDEFGHKL", pseudonymization_id="ABCDEFGHKL",
) )
@@ -115,6 +115,7 @@ TEST_ORDERPOSITION_RES = {
"item": 1, "item": 1,
"variation": None, "variation": None,
"price": "23.00", "price": "23.00",
"attendee_name_parts": {"full_name": "Peter", "_scheme": "full"},
"attendee_name": "Peter", "attendee_name": "Peter",
"attendee_email": None, "attendee_email": None,
"voucher": None, "voucher": None,
@@ -195,6 +196,7 @@ TEST_ORDER_RES = {
"is_business": False, "is_business": False,
"company": "Sample company", "company": "Sample company",
"name": "", "name": "",
"name_parts": {},
"street": "", "street": "",
"zipcode": "", "zipcode": "",
"city": "", "city": "",
@@ -703,7 +705,7 @@ def test_orderposition_delete(token_client, organizer, event, order, item, quest
item=item, item=item,
variation=None, variation=None,
price=Decimal("23"), price=Decimal("23"),
attendee_name="Peter", attendee_name_parts={"full_name": "Peter", "_scheme": "full"},
secret="foobar", secret="foobar",
pseudonymization_id="BAZ", pseudonymization_id="BAZ",
) )
@@ -1249,7 +1251,7 @@ ORDER_CREATE_PAYLOAD = {
"invoice_address": { "invoice_address": {
"is_business": False, "is_business": False,
"company": "Sample company", "company": "Sample company",
"name": "Fo", "name_parts": {"full_name": "Fo"},
"street": "Bar", "street": "Bar",
"zipcode": "", "zipcode": "",
"city": "Sample City", "city": "Sample City",
@@ -1263,7 +1265,7 @@ ORDER_CREATE_PAYLOAD = {
"item": 1, "item": 1,
"variation": None, "variation": None,
"price": "23.00", "price": "23.00",
"attendee_name": "Peter", "attendee_name_parts": {"full_name": "Peter"},
"attendee_email": None, "attendee_email": None,
"addon_to": None, "addon_to": None,
"answers": [ "answers": [
@@ -1306,10 +1308,13 @@ def test_order_create(token_client, organizer, event, item, quota, question):
assert fee.value == Decimal('0.25') assert fee.value == Decimal('0.25')
ia = o.invoice_address ia = o.invoice_address
assert ia.company == "Sample company" assert ia.company == "Sample company"
assert ia.name_parts == {"full_name": "Fo", "_scheme": "full"}
assert ia.name_cached == "Fo"
assert o.positions.count() == 1 assert o.positions.count() == 1
pos = o.positions.first() pos = o.positions.first()
assert pos.item == item assert pos.item == item
assert pos.price == Decimal("23.00") assert pos.price == Decimal("23.00")
assert pos.attendee_name_parts == {"full_name": "Peter", "_scheme": "full"}
answ = pos.answers.first() answ = pos.answers.first()
assert answ.question == question assert answ.question == question
assert answ.answer == "S" assert answ.answer == "S"
@@ -1332,6 +1337,54 @@ def test_order_create_invoice_address_optional(token_client, organizer, event, i
o.invoice_address 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 @pytest.mark.django_db
def test_order_create_code_optional(token_client, organizer, event, item, quota, question): def test_order_create_code_optional(token_client, organizer, event, item, quota, question):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD) 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, "item": item.pk,
"variation": None, "variation": None,
"price": "23.00", "price": "23.00",
"attendee_name": "Peter", "attendee_name_parts": {"full_name": "Peter"},
"attendee_email": None, "attendee_email": None,
"addon_to": None, "addon_to": None,
"answers": [], "answers": [],
@@ -1657,7 +1710,7 @@ def test_order_create_positionids_addons(token_client, organizer, event, item, q
"item": item.pk, "item": item.pk,
"variation": None, "variation": None,
"price": "23.00", "price": "23.00",
"attendee_name": "Peter", "attendee_name_parts": {"full_name": "Peter"},
"attendee_email": None, "attendee_email": None,
"addon_to": 1, "addon_to": 1,
"answers": [], "answers": [],
@@ -1685,7 +1738,7 @@ def test_order_create_positionid_validation(token_client, organizer, event, item
"item": item.pk, "item": item.pk,
"variation": None, "variation": None,
"price": "23.00", "price": "23.00",
"attendee_name": "Peter", "attendee_name_parts": {"full_name": "Peter"},
"attendee_email": None, "attendee_email": None,
"addon_to": None, "addon_to": None,
"answers": [], "answers": [],
@@ -1696,7 +1749,7 @@ def test_order_create_positionid_validation(token_client, organizer, event, item
"item": item.pk, "item": item.pk,
"variation": None, "variation": None,
"price": "23.00", "price": "23.00",
"attendee_name": "Peter", "attendee_name_parts": {"full_name": "Peter"},
"attendee_email": None, "attendee_email": None,
"addon_to": 2, "addon_to": 2,
"answers": [], "answers": [],
@@ -1727,7 +1780,7 @@ def test_order_create_positionid_validation(token_client, organizer, event, item
"item": item.pk, "item": item.pk,
"variation": None, "variation": None,
"price": "23.00", "price": "23.00",
"attendee_name": "Peter", "attendee_name_parts": {"full_name": "Peter"},
"attendee_email": None, "attendee_email": None,
"addon_to": None, "addon_to": None,
"answers": [], "answers": [],
@@ -1737,7 +1790,7 @@ def test_order_create_positionid_validation(token_client, organizer, event, item
"item": item.pk, "item": item.pk,
"variation": None, "variation": None,
"price": "23.00", "price": "23.00",
"attendee_name": "Peter", "attendee_name_parts": {"full_name": "Peter"},
"attendee_email": None, "attendee_email": None,
"addon_to": 2, "addon_to": 2,
"answers": [], "answers": [],
@@ -1761,7 +1814,7 @@ def test_order_create_positionid_validation(token_client, organizer, event, item
"item": item.pk, "item": item.pk,
"variation": None, "variation": None,
"price": "23.00", "price": "23.00",
"attendee_name": "Peter", "attendee_name_parts": {"full_name": "Peter"},
"attendee_email": None, "attendee_email": None,
"answers": [], "answers": [],
"subevent": None "subevent": None
@@ -1770,7 +1823,7 @@ def test_order_create_positionid_validation(token_client, organizer, event, item
"item": item.pk, "item": item.pk,
"variation": None, "variation": None,
"price": "23.00", "price": "23.00",
"attendee_name": "Peter", "attendee_name_parts": {"full_name": "Peter"},
"attendee_email": None, "attendee_email": None,
"answers": [], "answers": [],
"subevent": None "subevent": None
@@ -1797,7 +1850,7 @@ def test_order_create_positionid_validation(token_client, organizer, event, item
"item": item.pk, "item": item.pk,
"variation": None, "variation": None,
"price": "23.00", "price": "23.00",
"attendee_name": "Peter", "attendee_name_parts": {"full_name": "Peter"},
"attendee_email": None, "attendee_email": None,
"answers": [], "answers": [],
"subevent": None "subevent": None
@@ -1807,7 +1860,7 @@ def test_order_create_positionid_validation(token_client, organizer, event, item
"item": item.pk, "item": item.pk,
"variation": None, "variation": None,
"price": "23.00", "price": "23.00",
"attendee_name": "Peter", "attendee_name_parts": {"full_name": "Peter"},
"attendee_email": None, "attendee_email": None,
"answers": [], "answers": [],
"subevent": None "subevent": None
@@ -2066,7 +2119,7 @@ def test_order_create_quota_validation(token_client, organizer, event, item, quo
"item": item.pk, "item": item.pk,
"variation": None, "variation": None,
"price": "23.00", "price": "23.00",
"attendee_name": "Peter", "attendee_name_parts": {"full_name": "Peter"},
"attendee_email": None, "attendee_email": None,
"addon_to": None, "addon_to": None,
"answers": [], "answers": [],
@@ -2077,7 +2130,7 @@ def test_order_create_quota_validation(token_client, organizer, event, item, quo
"item": item.pk, "item": item.pk,
"variation": None, "variation": None,
"price": "23.00", "price": "23.00",
"attendee_name": "Peter", "attendee_name_parts": {"full_name": "Peter"},
"attendee_email": None, "attendee_email": None,
"addon_to": 1, "addon_to": 1,
"answers": [], "answers": [],

View File

@@ -121,7 +121,10 @@ def test_address_vat_id(env):
event, order = env event, order = env
event.settings.set('invoice_language', 'en') event.settings.set('invoice_language', 'en')
InvoiceAddress.objects.create(company='Acme Company', street='221B Baker Street', 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) country='', vat_id='UK1234567', order=order)
inv = generate_invoice(order) inv = generate_invoice(order)
assert inv.invoice_to == "Acme Company\nSherlock Holmes\n221B Baker Street\n12345 London\nUK\nVAT-ID: UK1234567" assert inv.invoice_to == "Acme Company\nSherlock Holmes\n221B Baker Street\n12345 London\nUK\nVAT-ID: UK1234567"

View File

@@ -34,7 +34,7 @@ def order(event):
default_price=Decimal('23.00'), admission=True) default_price=Decimal('23.00'), admission=True)
OrderPosition.objects.create( OrderPosition.objects.create(
order=o, item=ticket, variation=None, 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 return o

View File

@@ -309,7 +309,7 @@ class PaymentReminderTests(TestCase):
default_price=Decimal('23.00'), admission=True) default_price=Decimal('23.00'), admission=True)
self.op1 = OrderPosition.objects.create( self.op1 = OrderPosition.objects.create(
order=self.order, item=self.ticket, variation=None, 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 = [] djmail.outbox = []
@@ -357,7 +357,7 @@ class DownloadReminderTests(TestCase):
default_price=Decimal('23.00'), admission=True) default_price=Decimal('23.00'), admission=True)
self.op1 = OrderPosition.objects.create( self.op1 = OrderPosition.objects.create(
order=self.order, item=self.ticket, variation=None, 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 = [] djmail.outbox = []
@@ -411,11 +411,11 @@ class OrderChangeManagerTests(TestCase):
default_price=Decimal('12.00')) default_price=Decimal('12.00'))
self.op1 = OrderPosition.objects.create( self.op1 = OrderPosition.objects.create(
order=self.order, item=self.ticket, variation=None, 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( self.op2 = OrderPosition.objects.create(
order=self.order, item=self.ticket, variation=None, 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.ocm = OrderChangeManager(self.order, None)
self.quota = self.event.quotas.create(name='Test', size=None) self.quota = self.event.quotas.create(name='Test', size=None)

View File

@@ -154,11 +154,11 @@ def test_availability_date_order_relative_subevents(event):
) )
OrderPosition.objects.create( OrderPosition.objects.create(
order=order, item=ticket, variation=None, subevent=se1, 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( OrderPosition.objects.create(
order=order, item=ticket, variation=None, subevent=se2, 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) prov = DummyPaymentProvider(event)

View File

@@ -54,7 +54,7 @@ def order(event, item):
item=item, item=item,
variation=None, variation=None,
price=Decimal("14"), price=Decimal("14"),
attendee_name="Peter", attendee_name_parts={'full_name': "Peter", "_scheme": "full"},
attendee_email="foo@example.org" attendee_email="foo@example.org"
) )
return o return o
@@ -155,7 +155,7 @@ def test_attendee_name_shredder(event, order):
} }
s.shred_data() s.shred_data()
order.refresh_from_db() order.refresh_from_db()
assert order.positions.first().attendee_name is None assert not order.positions.first().attendee_name
l1.refresh_from_db() l1.refresh_from_db()
assert 'Hans' not in l1.data assert 'Hans' not in l1.data
assert 'Foo' in l1.data assert 'Foo' in l1.data
@@ -186,6 +186,7 @@ def test_invoice_address_shredder(event, order):
'is_business': False, 'is_business': False,
'last_modified': ia.last_modified.isoformat().replace('+00:00', 'Z'), 'last_modified': ia.last_modified.isoformat().replace('+00:00', 'Z'),
'name': '', 'name': '',
'name_parts': {},
'street': '221B Baker Street', 'street': '221B Baker Street',
'vat_id': '', 'vat_id': '',
'vat_id_validated': False, 'vat_id_validated': False,

View File

@@ -45,7 +45,7 @@ def dashboard_env():
item=item_ticket, item=item_ticket,
variation=None, variation=None,
price=Decimal("23"), price=Decimal("23"),
attendee_name="Peter" attendee_name_parts={"full_name": "Peter"}
) )
OrderPosition.objects.create( OrderPosition.objects.create(
order=order_paid, order=order_paid,
@@ -77,7 +77,7 @@ def test_dashboard_pending_not_count(dashboard_env):
item=dashboard_env[4], item=dashboard_env[4],
variation=None, variation=None,
price=Decimal("23"), price=Decimal("23"),
attendee_name="NotPaid" attendee_name_parts={'full_name': "NotPaid"}
) )
assert '0/2' in c[0]['content'] assert '0/2' in c[0]['content']
@@ -149,14 +149,14 @@ def checkin_list_env():
item=item_ticket, item=item_ticket,
variation=None, variation=None,
price=Decimal("23"), price=Decimal("23"),
attendee_name="Pending" attendee_name_parts={'full_name': "Pending"}
) )
op_a1_ticket = OrderPosition.objects.create( op_a1_ticket = OrderPosition.objects.create(
order=order_a1, order=order_a1,
item=item_ticket, item=item_ticket,
variation=None, variation=None,
price=Decimal("23"), price=Decimal("23"),
attendee_name="A1" attendee_name_parts={'full_name': "A1"}
) )
op_a1_mascot = OrderPosition.objects.create( op_a1_mascot = OrderPosition.objects.create(
order=order_a1, order=order_a1,
@@ -169,14 +169,14 @@ def checkin_list_env():
item=item_ticket, item=item_ticket,
variation=None, variation=None,
price=Decimal("23"), price=Decimal("23"),
attendee_name="A2" attendee_name_parts={'full_name': "A2"}
) )
op_a3_ticket = OrderPosition.objects.create( op_a3_ticket = OrderPosition.objects.create(
order=order_a3, order=order_a3,
item=item_ticket, item=item_ticket,
variation=None, variation=None,
price=Decimal("23"), 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" attendee_email="a3company@dummy.test"
) )
@@ -339,14 +339,14 @@ def checkin_list_with_addon_env():
item=item_ticket, item=item_ticket,
variation=None, variation=None,
price=Decimal("23"), price=Decimal("23"),
attendee_name="Pending" attendee_name_parts={'full_name': "Pending"}
) )
op_a1_ticket = OrderPosition.objects.create( op_a1_ticket = OrderPosition.objects.create(
order=order_a1, order=order_a1,
item=item_ticket, item=item_ticket,
variation=None, variation=None,
price=Decimal("23"), price=Decimal("23"),
attendee_name="A1" attendee_name_parts={'full_name': "A1"}
) )
op_a1_workshop = OrderPosition.objects.create( op_a1_workshop = OrderPosition.objects.create(
order=order_a1, order=order_a1,
@@ -360,7 +360,7 @@ def checkin_list_with_addon_env():
item=item_ticket, item=item_ticket,
variation=None, variation=None,
price=Decimal("23"), price=Decimal("23"),
attendee_name="A2" attendee_name_parts={'full_name': "A2"}
) )
# checkin # checkin

View File

@@ -187,13 +187,13 @@ class QuestionsTest(ItemFormTest):
expires=now() + datetime.timedelta(days=10), expires=now() + datetime.timedelta(days=10),
total=14, locale='en') total=14, locale='en')
op = OrderPosition.objects.create(order=o, item=item1, variation=None, price=Decimal("14"), 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.answers.create(question=c, answer='42')
op = OrderPosition.objects.create(order=o, item=item1, variation=None, price=Decimal("14"), 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.answers.create(question=c, answer='42')
op = OrderPosition.objects.create(order=o, item=item1, variation=None, price=Decimal("14"), 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') 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)) 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, item=self.item1,
variation=None, variation=None,
price=Decimal("14"), 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), self.client.post('/control/event/%s/%s/items/%d/delete' % (self.orga1.slug, self.event1.slug, self.item1.id),
{}) {})

View File

@@ -51,7 +51,7 @@ def env():
item=ticket, item=ticket,
variation=None, variation=None,
price=Decimal("14"), price=Decimal("14"),
attendee_name="Peter" attendee_name_parts={'full_name': "Peter", "_scheme": "full"}
) )
return event, user, o, ticket return event, user, o, ticket
@@ -333,7 +333,7 @@ def test_order_invoice_create_ok(client, env):
def test_order_invoice_regenerate(client, env): def test_order_invoice_regenerate(client, env):
client.login(email='dummy@dummy.dummy', password='dummy') client.login(email='dummy@dummy.dummy', password='dummy')
i = generate_invoice(env[2]) 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') env[0].settings.set('invoice_generate', 'admin')
response = client.post('/control/event/dummy/dummy/orders/FOO/invoices/%d/regenerate' % i.pk, {}, follow=True) response = client.post('/control/event/dummy/dummy/orders/FOO/invoices/%d/regenerate' % i.pk, {}, follow=True)
assert 'alert-success' in response.rendered_content 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): def test_order_invoice_reissue(client, env):
client.login(email='dummy@dummy.dummy', password='dummy') client.login(email='dummy@dummy.dummy', password='dummy')
i = generate_invoice(env[2]) 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') env[0].settings.set('invoice_generate', 'admin')
response = client.post('/control/event/dummy/dummy/orders/FOO/invoices/%d/reissue' % i.pk, {}, follow=True) response = client.post('/control/event/dummy/dummy/orders/FOO/invoices/%d/reissue' % i.pk, {}, follow=True)
assert 'alert-success' in response.rendered_content assert 'alert-success' in response.rendered_content
@@ -528,7 +528,7 @@ def test_order_extend_expired_quota_partial(client, env):
item=env[3], item=env[3],
variation=None, variation=None,
price=Decimal("14"), price=Decimal("14"),
attendee_name="Peter" attendee_name_parts={'full_name': "Peter", "_scheme": "full"}
) )
o.expires = now() - timedelta(days=5) o.expires = now() - timedelta(days=5)
o.status = Order.STATUS_EXPIRED o.status = Order.STATUS_EXPIRED
@@ -745,11 +745,11 @@ class OrderChangeTests(SoupTest):
default_price=Decimal('12.00')) default_price=Decimal('12.00'))
self.op1 = OrderPosition.objects.create( self.op1 = OrderPosition.objects.create(
order=self.order, item=self.ticket, variation=None, 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( self.op2 = OrderPosition.objects.create(
order=self.order, item=self.ticket, variation=None, 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 = self.event.quotas.create(name="All", size=100)
self.quota.items.add(self.ticket) self.quota.items.add(self.ticket)

View File

@@ -30,7 +30,7 @@ class OrderSearchTest(SoupTest):
datetime=now(), expires=now() + datetime.timedelta(days=10), datetime=now(), expires=now() + datetime.timedelta(days=10),
total=14, locale='en' 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', ticket1 = Item.objects.create(event=self.event1, name='Early-bird ticket',
category=None, default_price=23, category=None, default_price=23,
admission=True) admission=True)
@@ -39,7 +39,7 @@ class OrderSearchTest(SoupTest):
item=ticket1, item=ticket1,
variation=None, variation=None,
price=Decimal("14"), price=Decimal("14"),
attendee_name="Peter", attendee_name_parts={'full_name': "Peter", "_scheme": "full"},
attendee_email="att@att.com" attendee_email="att@att.com"
) )
@@ -57,7 +57,7 @@ class OrderSearchTest(SoupTest):
item=ticket2, item=ticket2,
variation=None, variation=None,
price=Decimal("14"), 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) self.team = Team.objects.create(organizer=self.orga1, can_view_orders=True)

View File

@@ -29,11 +29,11 @@ def env():
shirt_red = ItemVariation.objects.create(item=shirt, default_price=14, value="Red") shirt_red = ItemVariation.objects.create(item=shirt, default_price=14, value="Red")
OrderPosition.objects.create( OrderPosition.objects.create(
order=o1, item=shirt, variation=shirt_red, order=o1, item=shirt, variation=shirt_red,
price=12, attendee_name=None, secret='1234' price=12, attendee_name_parts={}, secret='1234'
) )
OrderPosition.objects.create( OrderPosition.objects.create(
order=o1, item=shirt, variation=shirt_red, 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 return event, o1, shirt

View File

@@ -36,11 +36,11 @@ def env():
) )
op1 = OrderPosition.objects.create( op1 = OrderPosition.objects.create(
order=o1, item=shirt, variation=shirt_red, 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( op2 = OrderPosition.objects.create(
order=o1, item=ticket, 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) cl1 = event.checkin_lists.create(name="Foo", all_products=True)
cl2 = event.checkin_lists.create(name="Bar", 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 @pytest.mark.django_db
def test_search_invoice_name(client, env): def test_search_invoice_name(client, env):
AppConfiguration.objects.create(event=env[0], key='abcdefg', list=env[5]) 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' % ( resp = client.get('/pretixdroid/api/%s/%s/search/?key=%s&query=%s' % (
env[0].organizer.slug, env[0].slug, 'abcdefg', 'John')) env[0].organizer.slug, env[0].slug, 'abcdefg', 'John'))
jdata = json.loads(resp.content.decode("utf-8")) jdata = json.loads(resp.content.decode("utf-8"))

View File

@@ -39,11 +39,11 @@ def env():
) )
op1 = OrderPosition.objects.create( op1 = OrderPosition.objects.create(
order=o1, item=shirt, variation=shirt_red, 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( op2 = OrderPosition.objects.create(
order=o1, item=ticket, 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) cl1 = event.checkin_lists.create(name="Foo", all_products=True, subevent=se1)
cl2 = event.checkin_lists.create(name="Foo", all_products=True, subevent=se2) cl2 = event.checkin_lists.create(name="Foo", all_products=True, subevent=se2)

View File

@@ -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"
""")

View File

@@ -29,11 +29,11 @@ def env0():
shirt_red = ItemVariation.objects.create(item=shirt, default_price=14, value="Red") shirt_red = ItemVariation.objects.create(item=shirt, default_price=14, value="Red")
OrderPosition.objects.create( OrderPosition.objects.create(
order=o1, item=shirt, variation=shirt_red, order=o1, item=shirt, variation=shirt_red,
price=12, attendee_name=None, secret='1234' price=12, attendee_name_parts={}, secret='1234'
) )
OrderPosition.objects.create( OrderPosition.objects.create(
order=o1, item=shirt, variation=shirt_red, order=o1, item=shirt, variation=shirt_red,
price=12, attendee_name=None, secret='5678' price=12, attendee_name_parts={}, secret='5678'
) )
return event, o1 return event, o1

View File

@@ -539,11 +539,11 @@ class CheckoutTestCase(TestCase):
) )
response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True) response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.rendered_content, "lxml") 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 # Not all required fields filled out, expect failure
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), { 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' 'email': 'admin@localhost'
}, follow=True) }, follow=True)
doc = BeautifulSoup(response.rendered_content, "lxml") doc = BeautifulSoup(response.rendered_content, "lxml")
@@ -551,7 +551,7 @@ class CheckoutTestCase(TestCase):
# Corrected request # Corrected request
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), { 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' 'email': 'admin@localhost'
}, follow=True) }, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), 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) cr1 = CartPosition.objects.get(id=cr1.id)
self.assertEqual(cr1.attendee_name, 'Peter') 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): def test_attendee_name_optional(self):
self.event.settings.set('attendee_names_asked', True) self.event.settings.set('attendee_names_asked', True)
self.event.settings.set('attendee_names_required', False) 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) response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.rendered_content, "lxml") 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 # Not all fields filled out, expect success
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), { 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' 'email': 'admin@localhost'
}, follow=True) }, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
target_status_code=200) target_status_code=200)
cr1 = CartPosition.objects.get(id=cr1.id) cr1 = CartPosition.objects.get(id=cr1.id)
self.assertIsNone(cr1.attendee_name) assert not cr1.attendee_name
def test_invoice_address_required(self): def test_invoice_address_required(self):
self.event.settings.invoice_address_asked = True self.event.settings.invoice_address_asked = True
self.event.settings.invoice_address_required = True self.event.settings.invoice_address_required = True
self.event.settings.set('name_scheme', 'title_given_middle_family')
CartPosition.objects.create( CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket, 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), { response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'is_business': 'business', 'is_business': 'business',
'company': 'Foo', 'company': 'Foo',
'name': 'Bar', 'name_parts_0': 'Mr',
'name_parts_1': 'John',
'name_parts_2': '',
'name_parts_3': 'Kennedy',
'street': 'Baz', 'street': 'Baz',
'zipcode': '12345', 'zipcode': '12345',
'city': 'Here', 'city': 'Here',
@@ -619,6 +659,15 @@ class CheckoutTestCase(TestCase):
}, follow=True) }, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
target_status_code=200) 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): def test_invoice_address_optional(self):
self.event.settings.invoice_address_asked = True 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) response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.rendered_content, "lxml") 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) self.assertEqual(len(doc.select('input[name=street]')), 0)
# Not all required fields filled out, expect failure # Not all required fields filled out, expect failure
@@ -665,7 +714,7 @@ class CheckoutTestCase(TestCase):
# Corrected request # Corrected request
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), { response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'name': 'Raphael', 'name_parts_0': 'Raphael',
'email': 'admin@localhost' 'email': 'admin@localhost'
}, follow=True) }, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), 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) self.event.settings.set('invoice_address_required', True)
ia = InvoiceAddress.objects.create( ia = InvoiceAddress.objects.create(
is_business=True, vat_id='ATU1234567', vat_id_validated=True, 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) self._set_session('invoice_address', ia.pk)
CartPosition.objects.create( CartPosition.objects.create(
@@ -786,7 +835,7 @@ class CheckoutTestCase(TestCase):
self.event.settings.set('invoice_address_required', True) self.event.settings.set('invoice_address_required', True)
ia = InvoiceAddress.objects.create( ia = InvoiceAddress.objects.create(
is_business=True, vat_id='ATU1234567', vat_id_validated=True, 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) self._set_session('invoice_address', ia.pk)
CartPosition.objects.create( CartPosition.objects.create(
@@ -828,7 +877,7 @@ class CheckoutTestCase(TestCase):
self.assertRedirects(response, '/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), self.assertRedirects(response, '/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug),
target_status_code=200) target_status_code=200)
cr1.attendee_name = 'Peter' cr1.attendee_name_parts = {"full_name": 'Peter', "_scheme": "full"}
cr1.save() cr1.save()
q1 = Question.objects.create( q1 = Question.objects.create(
event=self.event, question='Age', type=Question.TYPE_NUMBER, event=self.event, question='Age', type=Question.TYPE_NUMBER,

View File

@@ -61,7 +61,7 @@ class OrdersTest(TestCase):
item=self.ticket, item=self.ticket,
variation=None, variation=None,
price=Decimal("23"), price=Decimal("23"),
attendee_name="Peter" attendee_name_parts={'full_name': "Peter"}
) )
self.not_my_order = Order.objects.create( self.not_my_order = Order.objects.create(
status=Order.STATUS_PENDING, status=Order.STATUS_PENDING,
@@ -147,12 +147,12 @@ class OrdersTest(TestCase):
response = self.client.get( response = self.client.get(
'/%s/%s/order/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)) '/%s/%s/order/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret))
doc = BeautifulSoup(response.rendered_content, "lxml") 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 # Not all fields filled out, expect success
response = self.client.post( response = self.client.post(
'/%s/%s/order/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), { '/%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) }, follow=True)
self.assertRedirects(response, self.assertRedirects(response,
'/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code, '/%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( response = self.client.get(
'/%s/%s/order/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)) '/%s/%s/order/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret))
doc = BeautifulSoup(response.rendered_content, "lxml") 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 # Not all required fields filled out, expect failure
response = self.client.post( response = self.client.post(
'/%s/%s/order/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), { '/%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) }, follow=True)
doc = BeautifulSoup(response.rendered_content, "lxml") doc = BeautifulSoup(response.rendered_content, "lxml")
self.assertGreaterEqual(len(doc.select('.has-error')), 1) self.assertGreaterEqual(len(doc.select('.has-error')), 1)
response = self.client.post( response = self.client.post(
'/%s/%s/order/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), { '/%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) }, follow=True)
self.assertRedirects(response, '/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code, self.assertRedirects(response, '/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code,
self.order.secret), self.order.secret),

View File

@@ -30,7 +30,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
item=self.ticket, item=self.ticket,
variation=None, variation=None,
price=Decimal("23"), price=Decimal("23"),
attendee_name="Peter" attendee_name_parts={'full_name': "Peter"}
) )
def test_iframe_entry_view_wrapper(self): def test_iframe_entry_view_wrapper(self):