mirror of
https://github.com/pretix/pretix.git
synced 2025-12-21 16:42:26 +00:00
- [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:
@@ -11,7 +11,6 @@ fi
|
||||
|
||||
if [ "$PRETIX_CONFIG_FILE" == "tests/travis_postgres.cfg" ]; then
|
||||
psql -c 'create database travis_ci_test;' -U postgres
|
||||
pip3 install -Ur src/requirements/postgres.txt
|
||||
fi
|
||||
|
||||
if [ "$1" == "style" ]; then
|
||||
|
||||
@@ -32,6 +32,7 @@ matrix:
|
||||
env: JOB=translation-spelling
|
||||
addons:
|
||||
postgresql: "9.4"
|
||||
mariadb: '10.3'
|
||||
apt:
|
||||
packages:
|
||||
- enchant
|
||||
|
||||
@@ -30,7 +30,7 @@ RUN chmod +x /usr/local/bin/pretix && \
|
||||
pip3 install -U pip wheel setuptools && \
|
||||
cd /pretix/src && \
|
||||
rm -f pretix.cfg && \
|
||||
pip3 install -r requirements.txt -r requirements/mysql.txt -r requirements/postgres.txt \
|
||||
pip3 install -r requirements.txt -r requirements/mysql.txt \
|
||||
-r requirements/memcached.txt -r requirements/redis.txt gunicorn && \
|
||||
mkdir -p data && \
|
||||
chown -R pretixuser:pretixuser /pretix /data data && \
|
||||
|
||||
@@ -26,7 +26,7 @@ installation guides):
|
||||
* `Docker`_
|
||||
* A SMTP server to send out mails, e.g. `Postfix`_ on your machine or some third-party server you have credentials for
|
||||
* A HTTP reverse proxy, e.g. `nginx`_ or Apache to allow HTTPS connections
|
||||
* A `MySQL`_ or `PostgreSQL`_ database server
|
||||
* A `PostgreSQL`_, `MySQL`_ 5.7+, or MariaDB 10.2.7+ database server
|
||||
* A `redis`_ server
|
||||
|
||||
We also recommend that you use a firewall, although this is not a pretix-specific recommendation. If you're new to
|
||||
@@ -36,6 +36,9 @@ Linux and firewalls, we recommend that you start with `ufw`_.
|
||||
SSL certificates can be obtained for free these days. We also *do not* provide support for HTTP-only
|
||||
installations except for evaluation purposes.
|
||||
|
||||
.. warning:: We recommend **PostgreSQL**. If you go for MySQL, make sure you run **MySQL 5.7 or newer** or
|
||||
**MariaDB 10.2.7 or newer**.
|
||||
|
||||
On this guide
|
||||
-------------
|
||||
|
||||
@@ -58,7 +61,7 @@ Next, we need a database and a database user. We can create these with any kind
|
||||
our database's shell, e.g. for MySQL::
|
||||
|
||||
$ mysql -u root -p
|
||||
mysql> CREATE DATABASE pretix DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;
|
||||
mysql> CREATE DATABASE pretix DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
|
||||
mysql> GRANT ALL PRIVILEGES ON pretix.* TO pretix@'localhost' IDENTIFIED BY '*********';
|
||||
mysql> FLUSH PRIVILEGES;
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@ To use pretix, you will need the following things:
|
||||
|
||||
.. warning:: Do not ever use SQLite in production. It will break.
|
||||
|
||||
.. warning:: We recommend **PostgreSQL**. If you go for MySQL, make sure you run **MySQL 5.7 or newer** or
|
||||
**MariaDB 10.2.7 or newer**.
|
||||
|
||||
* A **reverse proxy**. pretix needs to deliver some static content to your users (e.g. CSS, images, ...). While pretix
|
||||
is capable of doing this, having this handled by a proper web server like **nginx** or **Apache** will be much
|
||||
faster. Also, you need a proxying web server in front to provide SSL encryption.
|
||||
|
||||
@@ -23,7 +23,7 @@ installation guides):
|
||||
|
||||
* A SMTP server to send out mails, e.g. `Postfix`_ on your machine or some third-party server you have credentials for
|
||||
* A HTTP reverse proxy, e.g. `nginx`_ or Apache to allow HTTPS connections
|
||||
* A `MySQL`_ or `PostgreSQL`_ database server
|
||||
* A `PostgreSQL`_, `MySQL`_ 5.7+, or MariaDB 10.2.7+ database server
|
||||
* A `redis`_ server
|
||||
|
||||
We also recommend that you use a firewall, although this is not a pretix-specific recommendation. If you're new to
|
||||
@@ -33,6 +33,9 @@ Linux and firewalls, we recommend that you start with `ufw`_.
|
||||
SSL certificates can be obtained for free these days. We also *do not* provide support for HTTP-only
|
||||
installations except for evaluation purposes.
|
||||
|
||||
.. warning:: We recommend **PostgreSQL**. If you go for MySQL, make sure you run **MySQL 5.7 or newer** or
|
||||
**MariaDB 10.2.7 or newer**.
|
||||
|
||||
Unix user
|
||||
---------
|
||||
|
||||
@@ -50,7 +53,7 @@ Having the database server installed, we still need a database and a database us
|
||||
of database managing tool or directly on our database's shell, e.g. for MySQL::
|
||||
|
||||
$ mysql -u root -p
|
||||
mysql> CREATE DATABASE pretix DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;
|
||||
mysql> CREATE DATABASE pretix DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
|
||||
mysql> GRANT ALL PRIVILEGES ON pretix.* TO pretix@'localhost' IDENTIFIED BY '*********';
|
||||
mysql> FLUSH PRIVILEGES;
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ item integer ID of the item
|
||||
variation integer ID of the variation (or ``null``)
|
||||
price money (string) Price of this position
|
||||
attendee_name string Specified attendee name for this position (or ``null``)
|
||||
attendee_name_parts object of strings Composition of attendee name (i.e. first name, last name, …)
|
||||
attendee_email string Specified attendee email address for this position (or ``null``)
|
||||
voucher integer Internal ID of the voucher used for this position (or ``null``)
|
||||
addon_to integer Internal ID of the position this position is an add-on for (or ``null``)
|
||||
@@ -78,6 +79,7 @@ Cart position endpoints
|
||||
"variation": null,
|
||||
"price": "23.00",
|
||||
"attendee_name": null,
|
||||
"attendee_name_parts": {},
|
||||
"attendee_email": null,
|
||||
"voucher": null,
|
||||
"addon_to": null,
|
||||
@@ -122,6 +124,7 @@ Cart position endpoints
|
||||
"variation": null,
|
||||
"price": "23.00",
|
||||
"attendee_name": null,
|
||||
"attendee_name_parts": {},
|
||||
"attendee_email": null,
|
||||
"voucher": null,
|
||||
"addon_to": null,
|
||||
@@ -175,7 +178,7 @@ Cart position endpoints
|
||||
* ``item``
|
||||
* ``variation`` (optional)
|
||||
* ``price``
|
||||
* ``attendee_name`` (optional)
|
||||
* ``attendee_name`` **or** ``attendee_name_parts`` (optional)
|
||||
* ``attendee_email`` (optional)
|
||||
* ``subevent`` (optional)
|
||||
* ``expires`` (optional)
|
||||
@@ -199,7 +202,10 @@ Cart position endpoints
|
||||
"item": 1,
|
||||
"variation": null,
|
||||
"price": "23.00",
|
||||
"attendee_name": "Peter",
|
||||
"attendee_name_parts": {
|
||||
"given_name": "Peter",
|
||||
"family_name": "Miller"
|
||||
},
|
||||
"attendee_email": null,
|
||||
"answers": [
|
||||
{
|
||||
|
||||
@@ -371,6 +371,9 @@ Order position endpoints
|
||||
"variation": null,
|
||||
"price": "23.00",
|
||||
"attendee_name": "Peter",
|
||||
"attendee_name_parts": {
|
||||
"full_name": "Peter",
|
||||
},
|
||||
"attendee_email": null,
|
||||
"voucher": null,
|
||||
"tax_rate": "0.00",
|
||||
@@ -466,6 +469,9 @@ Order position endpoints
|
||||
"variation": null,
|
||||
"price": "23.00",
|
||||
"attendee_name": "Peter",
|
||||
"attendee_name_parts": {
|
||||
"full_name": "Peter",
|
||||
},
|
||||
"attendee_email": null,
|
||||
"voucher": null,
|
||||
"tax_rate": "0.00",
|
||||
|
||||
@@ -46,6 +46,7 @@ invoice_address object Invoice address
|
||||
for orders created before pretix 1.7, do not rely on
|
||||
it).
|
||||
├ name string Customer name
|
||||
├ name_parts object of strings Customer name decomposition
|
||||
├ street string Customer street
|
||||
├ zipcode string Customer ZIP code
|
||||
├ city string Customer city
|
||||
@@ -137,6 +138,7 @@ item integer ID of the purch
|
||||
variation integer ID of the purchased variation (or ``null``)
|
||||
price money (string) Price of this position
|
||||
attendee_name string Specified attendee name for this position (or ``null``)
|
||||
attendee_name_parts object of strings Decomposition of attendee name (i.e. given name, family name)
|
||||
attendee_email string Specified attendee email address for this position (or ``null``)
|
||||
voucher integer Internal ID of the voucher used for this position (or ``null``)
|
||||
tax_rate decimal (string) VAT rate applied for this position
|
||||
@@ -278,6 +280,7 @@ List of all orders
|
||||
"is_business": True,
|
||||
"company": "Sample company",
|
||||
"name": "John Doe",
|
||||
"name_parts": {"full_name": "John Doe"},
|
||||
"street": "Test street 12",
|
||||
"zipcode": "12345",
|
||||
"city": "Testington",
|
||||
@@ -295,6 +298,9 @@ List of all orders
|
||||
"variation": null,
|
||||
"price": "23.00",
|
||||
"attendee_name": "Peter",
|
||||
"attendee_name_parts": {
|
||||
"full_name": "Peter",
|
||||
},
|
||||
"attendee_email": null,
|
||||
"voucher": null,
|
||||
"tax_rate": "0.00",
|
||||
@@ -410,6 +416,7 @@ Fetching individual orders
|
||||
"company": "Sample company",
|
||||
"is_business": True,
|
||||
"name": "John Doe",
|
||||
"name_parts": {"full_name": "John Doe"},
|
||||
"street": "Test street 12",
|
||||
"zipcode": "12345",
|
||||
"city": "Testington",
|
||||
@@ -427,6 +434,9 @@ Fetching individual orders
|
||||
"variation": null,
|
||||
"price": "23.00",
|
||||
"attendee_name": "Peter",
|
||||
"attendee_name_parts": {
|
||||
"full_name": "Peter",
|
||||
},
|
||||
"attendee_email": null,
|
||||
"voucher": null,
|
||||
"tax_rate": "0.00",
|
||||
@@ -601,7 +611,7 @@ Creating orders
|
||||
|
||||
* ``company``
|
||||
* ``is_business``
|
||||
* ``name``
|
||||
* ``name`` **or** ``name_parts``
|
||||
* ``street``
|
||||
* ``zipcode``
|
||||
* ``city``
|
||||
@@ -615,7 +625,7 @@ Creating orders
|
||||
* ``item``
|
||||
* ``variation``
|
||||
* ``price``
|
||||
* ``attendee_name``
|
||||
* ``attendee_name`` **or** ``attendee_name_parts``
|
||||
* ``attendee_email``
|
||||
* ``secret`` (optional)
|
||||
* ``addon_to`` (optional, see below)
|
||||
@@ -664,7 +674,7 @@ Creating orders
|
||||
"invoice_address": {
|
||||
"is_business": False,
|
||||
"company": "Sample company",
|
||||
"name": "John Doe",
|
||||
"name_parts": {"full_name": "John Doe"},
|
||||
"street": "Sesam Street 12",
|
||||
"zipcode": "12345",
|
||||
"city": "Sample City",
|
||||
@@ -678,7 +688,9 @@ Creating orders
|
||||
"item": 1,
|
||||
"variation": null,
|
||||
"price": "23.00",
|
||||
"attendee_name": "Peter",
|
||||
"attendee_name_parts": {
|
||||
"full_name": "Peter"
|
||||
},
|
||||
"attendee_email": null,
|
||||
"addon_to": null,
|
||||
"answers": [
|
||||
@@ -1075,6 +1087,9 @@ List of all order positions
|
||||
"variation": null,
|
||||
"price": "23.00",
|
||||
"attendee_name": "Peter",
|
||||
"attendee_name_parts": {
|
||||
"full_name": "Peter"
|
||||
},
|
||||
"attendee_email": null,
|
||||
"voucher": null,
|
||||
"tax_rate": "0.00",
|
||||
@@ -1172,6 +1187,9 @@ Fetching individual positions
|
||||
"variation": null,
|
||||
"price": "23.00",
|
||||
"attendee_name": "Peter",
|
||||
"attendee_name_parts": {
|
||||
"full_name": "Peter",
|
||||
},
|
||||
"attendee_email": null,
|
||||
"voucher": null,
|
||||
"tax_rate": "0.00",
|
||||
|
||||
@@ -19,18 +19,19 @@ class CartPositionSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = CartPosition
|
||||
fields = ('id', 'cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_email',
|
||||
'voucher', 'addon_to', 'subevent', 'datetime', 'expires', 'includes_tax',
|
||||
fields = ('id', 'cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
||||
'attendee_email', 'voucher', 'addon_to', 'subevent', 'datetime', 'expires', 'includes_tax',
|
||||
'answers',)
|
||||
|
||||
|
||||
class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
answers = AnswerCreateSerializer(many=True, required=False)
|
||||
expires = serializers.DateTimeField(required=False)
|
||||
attendee_name = serializers.CharField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = CartPosition
|
||||
fields = ('cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_email',
|
||||
fields = ('cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
||||
'subevent', 'expires', 'includes_tax', 'answers',)
|
||||
|
||||
def create(self, validated_data):
|
||||
@@ -65,6 +66,11 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
quota.name
|
||||
)
|
||||
)
|
||||
attendee_name = validated_data.pop('attendee_name', '')
|
||||
if attendee_name and not validated_data.get('attendee_name_parts'):
|
||||
validated_data['attendee_name_parts'] = {
|
||||
'_legacy': attendee_name
|
||||
}
|
||||
cp = CartPosition.objects.create(event=self.context['event'], **validated_data)
|
||||
|
||||
for answ_data in answers_data:
|
||||
@@ -118,4 +124,8 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
raise ValidationError(
|
||||
'You cannot specify a variation for this item.'
|
||||
)
|
||||
if data.get('attendee_name') and data.get('attendee_name_parts'):
|
||||
raise ValidationError(
|
||||
{'attendee_name': ['Do not specify attendee_name if you specified attendee_name_parts.']}
|
||||
)
|
||||
return data
|
||||
|
||||
@@ -35,11 +35,12 @@ class CompatibleCountryField(serializers.Field):
|
||||
|
||||
class InvoiceAddressSerializer(I18nAwareModelSerializer):
|
||||
country = CompatibleCountryField(source='*')
|
||||
name = serializers.CharField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = InvoiceAddress
|
||||
fields = ('last_modified', 'is_business', 'company', 'name', 'street', 'zipcode', 'city', 'country', 'vat_id',
|
||||
'vat_id_validated', 'internal_reference')
|
||||
fields = ('last_modified', 'is_business', 'company', 'name', 'name_parts', 'street', 'zipcode', 'city', 'country',
|
||||
'vat_id', 'vat_id_validated', 'internal_reference')
|
||||
read_only_fields = ('last_modified', 'vat_id_validated')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -48,6 +49,15 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
|
||||
v.required = False
|
||||
v.allow_blank = True
|
||||
|
||||
def validate(self, data):
|
||||
if data.get('name') and data.get('name_parts'):
|
||||
raise ValidationError(
|
||||
{'name': ['Do not specify name if you specified name_parts.']}
|
||||
)
|
||||
if data.get('name_parts') and '_scheme' not in data.get('name_parts'):
|
||||
data['name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
|
||||
return data
|
||||
|
||||
|
||||
class AnswerQuestionIdentifierField(serializers.Field):
|
||||
def to_representation(self, instance: QuestionAnswer):
|
||||
@@ -158,9 +168,9 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_email',
|
||||
'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins', 'downloads',
|
||||
'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data')
|
||||
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -305,10 +315,11 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
answers = AnswerCreateSerializer(many=True, required=False)
|
||||
addon_to = serializers.IntegerField(required=False, allow_null=True)
|
||||
secret = serializers.CharField(required=False)
|
||||
attendee_name = serializers.CharField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_email',
|
||||
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
||||
'secret', 'addon_to', 'subevent', 'answers')
|
||||
|
||||
def validate_secret(self, secret):
|
||||
@@ -359,6 +370,12 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
raise ValidationError(
|
||||
{'variation': ['You cannot specify a variation for this item.']}
|
||||
)
|
||||
if data.get('attendee_name') and data.get('attendee_name_parts'):
|
||||
raise ValidationError(
|
||||
{'attendee_name': ['Do not specify attendee_name if you specified attendee_name_parts.']}
|
||||
)
|
||||
if data.get('attendee_name_parts') and '_scheme' not in data.get('attendee_name_parts'):
|
||||
data['attendee_name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
|
||||
return data
|
||||
|
||||
|
||||
@@ -464,7 +481,13 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
payment_info = validated_data.pop('payment_info', '{}')
|
||||
|
||||
if 'invoice_address' in validated_data:
|
||||
ia = InvoiceAddress(**validated_data.pop('invoice_address'))
|
||||
iadata = validated_data.pop('invoice_address')
|
||||
name = iadata.pop('name', '')
|
||||
if name and not iadata.get('name_parts'):
|
||||
iadata['name_parts'] = {
|
||||
'_legacy': name
|
||||
}
|
||||
ia = InvoiceAddress(**iadata)
|
||||
else:
|
||||
ia = None
|
||||
|
||||
@@ -555,6 +578,11 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
for pos_data in positions_data:
|
||||
answers_data = pos_data.pop('answers', [])
|
||||
addon_to = pos_data.pop('addon_to', None)
|
||||
attendee_name = pos_data.pop('attendee_name', '')
|
||||
if attendee_name and not pos_data.get('attendee_name_parts'):
|
||||
pos_data['attendee_name_parts'] = {
|
||||
'_legacy': attendee_name
|
||||
}
|
||||
pos = OrderPosition(**pos_data)
|
||||
pos.order = order
|
||||
pos._calculate_tax()
|
||||
|
||||
@@ -154,7 +154,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = OrderPositionSerializer
|
||||
queryset = OrderPosition.objects.none()
|
||||
filter_backends = (DjangoFilterBackend, RichOrderingFilter)
|
||||
ordering = ('attendee_name', 'positionid')
|
||||
ordering = ('attendee_name_cached', 'positionid')
|
||||
ordering_fields = (
|
||||
'order__code', 'order__datetime', 'positionid', 'attendee_name',
|
||||
'last_checked_in', 'order__email',
|
||||
@@ -162,11 +162,11 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
ordering_custom = {
|
||||
'attendee_name': {
|
||||
'_order': F('display_name').asc(nulls_first=True),
|
||||
'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')
|
||||
'display_name': Coalesce('attendee_name_cached', 'addon_to__attendee_name_cached')
|
||||
},
|
||||
'-attendee_name': {
|
||||
'_order': F('display_name').desc(nulls_last=True),
|
||||
'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')
|
||||
'display_name': Coalesce('attendee_name_cached', 'addon_to__attendee_name_cached')
|
||||
},
|
||||
'last_checked_in': {
|
||||
'_order': FixedOrderBy(F('last_checked_in'), nulls_first=True),
|
||||
|
||||
@@ -3,8 +3,8 @@ import datetime
|
||||
import django_filters
|
||||
import pytz
|
||||
from django.db import transaction
|
||||
from django.db.models import Prefetch, Q
|
||||
from django.db.models.functions import Concat
|
||||
from django.db.models import F, Prefetch, Q
|
||||
from django.db.models.functions import Coalesce, Concat
|
||||
from django.http import FileResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.timezone import make_aware, now
|
||||
@@ -373,17 +373,17 @@ class OrderPositionFilter(FilterSet):
|
||||
def search_qs(self, queryset, name, value):
|
||||
return queryset.filter(
|
||||
Q(secret__istartswith=value)
|
||||
| Q(attendee_name__icontains=value)
|
||||
| Q(addon_to__attendee_name__icontains=value)
|
||||
| Q(attendee_name_cached__icontains=value)
|
||||
| Q(addon_to__attendee_name_cached__icontains=value)
|
||||
| Q(order__code__istartswith=value)
|
||||
| Q(order__invoice_address__name__icontains=value)
|
||||
| Q(order__invoice_address__name_cached__icontains=value)
|
||||
)
|
||||
|
||||
def has_checkin_qs(self, queryset, name, value):
|
||||
return queryset.filter(checkins__isnull=not value)
|
||||
|
||||
def attendee_name_qs(self, queryset, name, value):
|
||||
return queryset.filter(Q(attendee_name__iexact=value) | Q(addon_to__attendee_name__iexact=value))
|
||||
return queryset.filter(Q(attendee_name_cached__iexact=value) | Q(addon_to__attendee_name_cached__iexact=value))
|
||||
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
@@ -409,6 +409,16 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
|
||||
filterset_class = OrderPositionFilter
|
||||
permission = 'can_view_orders'
|
||||
write_permission = 'can_change_orders'
|
||||
ordering_custom = {
|
||||
'attendee_name': {
|
||||
'_order': F('display_name').asc(nulls_first=True),
|
||||
'display_name': Coalesce('attendee_name_cached', 'addon_to__attendee_name_cached')
|
||||
},
|
||||
'-attendee_name': {
|
||||
'_order': F('display_name').asc(nulls_last=True),
|
||||
'display_name': Coalesce('attendee_name_cached', 'addon_to__attendee_name_cached')
|
||||
},
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
return OrderPosition.objects.filter(order__event=self.request.event).prefetch_related(
|
||||
|
||||
@@ -12,6 +12,7 @@ from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
|
||||
from pretix.base.models import InvoiceAddress, Order, OrderPosition
|
||||
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
|
||||
from ..exporter import BaseExporter
|
||||
from ..signals import register_data_exporters
|
||||
@@ -74,7 +75,14 @@ class OrderListExporter(BaseExporter):
|
||||
|
||||
headers = [
|
||||
_('Order code'), _('Order total'), _('Status'), _('Email'), _('Order date'),
|
||||
_('Company'), _('Name'), _('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
|
||||
_('Company'), _('Name'),
|
||||
]
|
||||
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
|
||||
if len(name_scheme['fields']) > 1:
|
||||
for k, label, w in name_scheme['fields']:
|
||||
headers.append(label)
|
||||
headers += [
|
||||
_('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
|
||||
_('Date of last payment'), _('Fees'), _('Order locale')
|
||||
]
|
||||
|
||||
@@ -118,6 +126,13 @@ class OrderListExporter(BaseExporter):
|
||||
row += [
|
||||
order.invoice_address.company,
|
||||
order.invoice_address.name,
|
||||
]
|
||||
if len(name_scheme['fields']) > 1:
|
||||
for k, label, w in name_scheme['fields']:
|
||||
row.append(
|
||||
order.invoice_address.name_parts.get(k, '')
|
||||
)
|
||||
row += [
|
||||
order.invoice_address.street,
|
||||
order.invoice_address.zipcode,
|
||||
order.invoice_address.city,
|
||||
@@ -126,7 +141,7 @@ class OrderListExporter(BaseExporter):
|
||||
order.invoice_address.vat_id,
|
||||
]
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
row += ['', '', '', '', '', '', '']
|
||||
row += [''] * 7 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0)
|
||||
|
||||
row += [
|
||||
order.payment_date.astimezone(tz).strftime('%Y-%m-%d') if order.payment_date else '',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import copy
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
|
||||
@@ -8,6 +9,7 @@ import vat_moss.id
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.forms.widgets import (
|
||||
@@ -16,6 +18,7 @@ from pretix.base.forms.widgets import (
|
||||
)
|
||||
from pretix.base.models import InvoiceAddress, Question
|
||||
from pretix.base.models.tax import EU_COUNTRIES
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.control.forms import SplitDateTimeField
|
||||
from pretix.helpers.i18n import get_format_without_seconds
|
||||
from pretix.presale.signals import question_form_fields
|
||||
@@ -23,6 +26,103 @@ from pretix.presale.signals import question_form_fields
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NamePartsWidget(forms.MultiWidget):
|
||||
widget = forms.TextInput
|
||||
|
||||
def __init__(self, scheme: dict, field: forms.Field, attrs=None):
|
||||
widgets = []
|
||||
self.scheme = scheme
|
||||
self.field = field
|
||||
for fname, label, size in self.scheme['fields']:
|
||||
a = copy.copy(attrs) or {}
|
||||
a['data-fname'] = fname
|
||||
widgets.append(self.widget(attrs=a))
|
||||
super().__init__(widgets, attrs)
|
||||
|
||||
def decompress(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
data = []
|
||||
for i, field in enumerate(self.scheme['fields']):
|
||||
fname, label, size = field
|
||||
data.append(value.get(fname, ""))
|
||||
if '_legacy' in value and not data[-1]:
|
||||
data[-1] = value.get('_legacy', '')
|
||||
return data
|
||||
|
||||
def render(self, name: str, value, attrs=None, renderer=None) -> str:
|
||||
if not isinstance(value, list):
|
||||
value = self.decompress(value)
|
||||
output = []
|
||||
final_attrs = self.build_attrs(attrs or dict())
|
||||
if 'required' in final_attrs:
|
||||
del final_attrs['required']
|
||||
id_ = final_attrs.get('id', None)
|
||||
for i, widget in enumerate(self.widgets):
|
||||
try:
|
||||
widget_value = value[i]
|
||||
except (IndexError, TypeError):
|
||||
widget_value = None
|
||||
if id_:
|
||||
final_attrs = dict(
|
||||
final_attrs,
|
||||
id='%s_%s' % (id_, i),
|
||||
title=self.scheme['fields'][i][1],
|
||||
placeholder=self.scheme['fields'][i][1],
|
||||
)
|
||||
final_attrs['data-size'] = self.scheme['fields'][i][2]
|
||||
output.append(widget.render(name + '_%s' % i, widget_value, final_attrs, renderer=renderer))
|
||||
return mark_safe(self.format_output(output))
|
||||
|
||||
def format_output(self, rendered_widgets) -> str:
|
||||
return '<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):
|
||||
"""
|
||||
This form class is responsible for asking order-related questions. This includes
|
||||
@@ -47,10 +147,12 @@ class BaseQuestionsForm(forms.Form):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if item.admission and event.settings.attendee_names_asked:
|
||||
self.fields['attendee_name'] = forms.CharField(
|
||||
max_length=255, required=event.settings.attendee_names_required,
|
||||
self.fields['attendee_name_parts'] = NamePartsFormField(
|
||||
max_length=255,
|
||||
required=event.settings.attendee_names_required,
|
||||
scheme=event.settings.name_scheme,
|
||||
label=_('Attendee name'),
|
||||
initial=(cartpos.attendee_name if cartpos else orderpos.attendee_name),
|
||||
initial=(cartpos.attendee_name_parts if cartpos else orderpos.attendee_name_parts),
|
||||
)
|
||||
if item.admission and event.settings.attendee_emails_asked:
|
||||
self.fields['attendee_email'] = forms.EmailField(
|
||||
@@ -170,13 +272,12 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = InvoiceAddress
|
||||
fields = ('is_business', 'company', 'name', 'street', 'zipcode', 'city', 'country', 'vat_id',
|
||||
fields = ('is_business', 'company', 'name_parts', 'street', 'zipcode', 'city', 'country', 'vat_id',
|
||||
'internal_reference')
|
||||
widgets = {
|
||||
'is_business': BusinessBooleanRadio,
|
||||
'street': forms.Textarea(attrs={'rows': 2, 'placeholder': _('Street and Number')}),
|
||||
'company': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}),
|
||||
'name': forms.TextInput(attrs={}),
|
||||
'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}),
|
||||
'internal_reference': forms.TextInput,
|
||||
}
|
||||
@@ -191,15 +292,13 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
super().__init__(*args, **kwargs)
|
||||
if not event.settings.invoice_address_vatid:
|
||||
del self.fields['vat_id']
|
||||
|
||||
if not event.settings.invoice_address_required:
|
||||
for k, f in self.fields.items():
|
||||
f.required = False
|
||||
f.widget.is_required = False
|
||||
if 'required' in f.widget.attrs:
|
||||
del f.widget.attrs['required']
|
||||
|
||||
if event.settings.invoice_name_required:
|
||||
self.fields['name'].required = True
|
||||
elif event.settings.invoice_address_company_required:
|
||||
self.initial['is_business'] = True
|
||||
|
||||
@@ -210,20 +309,34 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
del self.fields['company'].widget.attrs['data-display-dependency']
|
||||
if 'vat_id' in self.fields:
|
||||
del self.fields['vat_id'].widget.attrs['data-display-dependency']
|
||||
else:
|
||||
|
||||
self.fields['name_parts'] = NamePartsFormField(
|
||||
max_length=255,
|
||||
required=event.settings.invoice_name_required,
|
||||
scheme=event.settings.name_scheme,
|
||||
label=_('Name'),
|
||||
initial=(self.instance.name_parts if self.instance else self.instance.name_parts),
|
||||
)
|
||||
if event.settings.invoice_address_required and not event.settings.invoice_address_company_required:
|
||||
self.fields['name_parts'].widget.attrs['data-required-if'] = '#id_is_business_0'
|
||||
self.fields['name_parts'].widget.attrs['data-no-required-attr'] = '1'
|
||||
self.fields['company'].widget.attrs['data-required-if'] = '#id_is_business_1'
|
||||
self.fields['name'].widget.attrs['data-required-if'] = '#id_is_business_0'
|
||||
|
||||
def clean(self):
|
||||
data = self.cleaned_data
|
||||
if not data.get('is_business'):
|
||||
data['company'] = ''
|
||||
if not data.get('name') and not data.get('company') and self.event.settings.invoice_address_required:
|
||||
raise ValidationError(_('You need to provide either a company name or your name.'))
|
||||
if self.event.settings.invoice_address_required:
|
||||
if data.get('is_business') and not data.get('company'):
|
||||
raise ValidationError(_('You need to provide a company name.'))
|
||||
if not data.get('is_business') and not data.get('name_parts'):
|
||||
raise ValidationError(_('You need to provide your name.'))
|
||||
|
||||
if 'vat_id' in self.changed_data or not data.get('vat_id'):
|
||||
self.instance.vat_id_validated = False
|
||||
|
||||
self.instance.name_parts = data.get('name_parts')
|
||||
|
||||
if self.validate_vat_id and self.instance.vat_id_validated and 'vat_id' not in self.changed_data:
|
||||
pass
|
||||
elif self.validate_vat_id and data.get('is_business') and data.get('country') in EU_COUNTRIES and data.get('vat_id'):
|
||||
|
||||
@@ -28,7 +28,8 @@ class Migration(migrations.Migration):
|
||||
('password', models.CharField(verbose_name='password', max_length=128)),
|
||||
('last_login', models.DateTimeField(verbose_name='last login', blank=True, null=True)),
|
||||
('is_superuser', models.BooleanField(verbose_name='superuser status', default=False, help_text='Designates that this user has all permissions without explicitly assigning them.')),
|
||||
('email', models.EmailField(max_length=254, blank=True, unique=True, verbose_name='E-mail', null=True, db_index=True)),
|
||||
('email', models.EmailField(max_length=191, blank=True, unique=True, verbose_name='E-mail', null=True,
|
||||
db_index=True)),
|
||||
('givenname', models.CharField(verbose_name='Given name', max_length=255, blank=True, null=True)),
|
||||
('familyname', models.CharField(verbose_name='Family name', max_length=255, blank=True, null=True)),
|
||||
('is_active', models.BooleanField(verbose_name='Is active', default=True)),
|
||||
|
||||
62
src/pretix/base/migrations/0102_auto_20181017_0024.py
Normal file
62
src/pretix/base/migrations/0102_auto_20181017_0024.py
Normal 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)
|
||||
]
|
||||
@@ -75,7 +75,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
REQUIRED_FIELDS = []
|
||||
|
||||
email = models.EmailField(unique=True, db_index=True, null=True, blank=True,
|
||||
verbose_name=_('E-mail'))
|
||||
verbose_name=_('E-mail'), max_length=190)
|
||||
fullname = models.CharField(max_length=255, blank=True, null=True,
|
||||
verbose_name=_('Full name'))
|
||||
is_active = models.BooleanField(default=True,
|
||||
|
||||
@@ -26,10 +26,12 @@ from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from django_countries.fields import CountryField
|
||||
from i18nfield.strings import LazyI18nString
|
||||
from jsonfallback.fields import FallbackJSONField
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import User
|
||||
from pretix.base.reldate import RelativeDateWrapper
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
|
||||
from .base import LockModel, LoggedModel
|
||||
from .event import Event, SubEvent
|
||||
@@ -699,8 +701,10 @@ class AbstractPosition(models.Model):
|
||||
:type expires: datetime
|
||||
:param price: The price of this item
|
||||
:type price: decimal.Decimal
|
||||
:param attendee_name: The attendee's name, if entered.
|
||||
:type attendee_name: str
|
||||
:param attendee_name_parts: The parts of the attendee's name, if entered.
|
||||
:type attendee_name_parts: str
|
||||
:param attendee_name_cached: The concatenated version of the attendee's name, if entered.
|
||||
:type attendee_name_cached: str
|
||||
:param attendee_email: The attendee's email, if entered.
|
||||
:type attendee_email: str
|
||||
:param voucher: A voucher that has been applied to this sale
|
||||
@@ -729,12 +733,15 @@ class AbstractPosition(models.Model):
|
||||
decimal_places=2, max_digits=10,
|
||||
verbose_name=_("Price")
|
||||
)
|
||||
attendee_name = models.CharField(
|
||||
attendee_name_cached = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("Attendee name"),
|
||||
blank=True, null=True,
|
||||
help_text=_("Empty, if this product is not an admission ticket")
|
||||
)
|
||||
attendee_name_parts = FallbackJSONField(
|
||||
blank=True, default=dict
|
||||
)
|
||||
attendee_email = models.EmailField(
|
||||
verbose_name=_("Attendee email"),
|
||||
blank=True, null=True,
|
||||
@@ -797,6 +804,24 @@ class AbstractPosition(models.Model):
|
||||
if self.variation is None
|
||||
else self.variation.quotas.filter(subevent=self.subevent))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.attendee_name_cached = self.attendee_name
|
||||
if self.attendee_name_parts is None:
|
||||
self.attendee_name_parts = {}
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def attendee_name(self):
|
||||
if not self.attendee_name_parts:
|
||||
return None
|
||||
if '_legacy' in self.attendee_name_parts:
|
||||
return self.attendee_name_parts['_legacy']
|
||||
if '_scheme' in self.attendee_name_parts:
|
||||
scheme = PERSON_NAME_SCHEMES[self.attendee_name_parts['_scheme']]
|
||||
else:
|
||||
scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
|
||||
return scheme['concatenation'](self.attendee_name_parts).strip()
|
||||
|
||||
|
||||
class OrderPayment(models.Model):
|
||||
"""
|
||||
@@ -1482,6 +1507,10 @@ class OrderPosition(AbstractPosition):
|
||||
self.pseudonymization_id = code
|
||||
return
|
||||
|
||||
@property
|
||||
def event(self):
|
||||
return self.order.event
|
||||
|
||||
|
||||
class CartPosition(AbstractPosition):
|
||||
"""
|
||||
@@ -1547,7 +1576,8 @@ class InvoiceAddress(models.Model):
|
||||
order = models.OneToOneField(Order, null=True, blank=True, related_name='invoice_address', on_delete=models.CASCADE)
|
||||
is_business = models.BooleanField(default=False, verbose_name=_('Business customer'))
|
||||
company = models.CharField(max_length=255, blank=True, verbose_name=_('Company name'))
|
||||
name = models.CharField(max_length=255, verbose_name=_('Full name'), blank=True)
|
||||
name_cached = models.CharField(max_length=255, verbose_name=_('Full name'), blank=True)
|
||||
name_parts = FallbackJSONField(default=dict)
|
||||
street = models.TextField(verbose_name=_('Address'), blank=False)
|
||||
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=False)
|
||||
city = models.CharField(max_length=255, verbose_name=_('City'), blank=False)
|
||||
@@ -1565,8 +1595,25 @@ class InvoiceAddress(models.Model):
|
||||
def save(self, **kwargs):
|
||||
if self.order:
|
||||
self.order.touch()
|
||||
|
||||
if self.name_parts:
|
||||
self.name_cached = self.name
|
||||
else:
|
||||
self.name_cached = ""
|
||||
super().save(**kwargs)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
if not self.name_parts:
|
||||
return ""
|
||||
if '_legacy' in self.name_parts:
|
||||
return self.name_parts['_legacy']
|
||||
if '_scheme' in self.name_parts:
|
||||
scheme = PERSON_NAME_SCHEMES[self.name_parts['_scheme']]
|
||||
else:
|
||||
raise TypeError("Invalid name given.")
|
||||
return scheme['concatenation'](self.name_parts).strip()
|
||||
|
||||
|
||||
def cachedticket_name(instance, filename: str) -> str:
|
||||
secret = get_random_string(length=16, allowed_chars=string.ascii_letters + string.digits)
|
||||
|
||||
@@ -26,6 +26,7 @@ from reportlab.platypus import Paragraph
|
||||
|
||||
from pretix.base.invoice import ThumbnailingImageReader
|
||||
from pretix.base.models import Order, OrderPosition
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.base.signals import layout_text_variables
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.presale.style import get_fonts
|
||||
@@ -147,12 +148,12 @@ DEFAULT_VARIABLES = OrderedDict((
|
||||
"evaluate": lambda op, order, ev: str(ev.location).replace("\n", "<br/>\n")
|
||||
}),
|
||||
("invoice_name", {
|
||||
"label": _("Invoice address: name"),
|
||||
"label": _("Invoice address name"),
|
||||
"editor_sample": _("John Doe"),
|
||||
"evaluate": lambda op, order, ev: order.invoice_address.name if getattr(order, 'invoice_address', None) else ''
|
||||
}),
|
||||
("invoice_company", {
|
||||
"label": _("Invoice address: company"),
|
||||
"label": _("Invoice address company"),
|
||||
"editor_sample": _("Sample company"),
|
||||
"evaluate": lambda op, order, ev: order.invoice_address.company if getattr(order, 'invoice_address', None) else ''
|
||||
}),
|
||||
@@ -182,8 +183,28 @@ DEFAULT_VARIABLES = OrderedDict((
|
||||
|
||||
def get_variables(event):
|
||||
v = copy.copy(DEFAULT_VARIABLES)
|
||||
|
||||
scheme = PERSON_NAME_SCHEMES[event.settings.name_scheme]
|
||||
for key, label, weight in scheme['fields']:
|
||||
v['attendee_name_%s' % key] = {
|
||||
'label': _("Attendee name: {part}").format(part=label),
|
||||
'editor_sample': scheme['sample'][key],
|
||||
'evaluate': lambda op, order, ev: op.attendee_name_parts.get(key, '')
|
||||
}
|
||||
|
||||
v['invoice_name']['editor_sample'] = scheme['concatenation'](scheme['sample'])
|
||||
v['attendee_name']['editor_sample'] = scheme['concatenation'](scheme['sample'])
|
||||
|
||||
for key, label, weight in scheme['fields']:
|
||||
v['invoice_name_%s' % key] = {
|
||||
'label': _("Invoice address name: {part}").format(part=label),
|
||||
'editor_sample': scheme['sample'][key],
|
||||
"evaluate": lambda op, order, ev: order.invoice_address.name_parts.get(key, '') if getattr(order, 'invoice_address', None) else ''
|
||||
}
|
||||
|
||||
for recv, res in layout_text_variables.send(sender=event):
|
||||
v.update(res)
|
||||
|
||||
return v
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from pretix.base.models import (
|
||||
OrderPosition,
|
||||
)
|
||||
from pretix.base.services.tasks import ProfiledTask
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.base.signals import allow_ticket_download, register_ticket_outputs
|
||||
from pretix.celery_app import app
|
||||
from pretix.helpers.database import rolledback_transaction
|
||||
@@ -87,11 +88,13 @@ def preview(event: int, provider: str):
|
||||
locale=event.settings.locale,
|
||||
expires=now(), code="PREVIEW1234", total=119)
|
||||
|
||||
p = order.positions.create(item=item, attendee_name=_("John Doe"), price=item.default_price)
|
||||
order.positions.create(item=item2, attendee_name=_("John Doe"), price=item.default_price, addon_to=p)
|
||||
order.positions.create(item=item2, attendee_name=_("John Doe"), price=item.default_price, addon_to=p)
|
||||
scheme = PERSON_NAME_SCHEMES[event.settings.name_scheme]
|
||||
sample = {k: str(v) for k, v in scheme['sample'].items()}
|
||||
p = order.positions.create(item=item, attendee_name_parts=sample, price=item.default_price)
|
||||
order.positions.create(item=item2, attendee_name_parts=sample, price=item.default_price, addon_to=p)
|
||||
order.positions.create(item=item2, attendee_name_parts=sample, price=item.default_price, addon_to=p)
|
||||
|
||||
InvoiceAddress.objects.create(order=order, name=_("John Doe"), company=_("Sample company"))
|
||||
InvoiceAddress.objects.create(order=order, name_parts=sample, company=_("Sample company"))
|
||||
|
||||
responses = register_ticket_outputs.send(event)
|
||||
for receiver, response in responses:
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import json
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files import File
|
||||
from django.db.models import Model
|
||||
from django.utils.translation import ugettext_noop
|
||||
from django.utils.translation import (
|
||||
pgettext_lazy, ugettext_lazy as _, ugettext_noop,
|
||||
)
|
||||
from hierarkey.models import GlobalSettingsBase, Hierarkey
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
@@ -556,7 +559,144 @@ Your {event} team"""))
|
||||
'default': 'date_ascending',
|
||||
'type': str
|
||||
},
|
||||
'name_scheme': {
|
||||
'default': 'full',
|
||||
'type': str
|
||||
}
|
||||
}
|
||||
PERSON_NAME_SCHEMES = OrderedDict([
|
||||
('given_family', {
|
||||
'fields': (
|
||||
('given_name', _('Given name'), 1),
|
||||
('family_name', _('Family name'), 1),
|
||||
),
|
||||
'concatenation': lambda d: ' '.join(str(p) for p in [d.get('given_name', ''), d.get('family_name', '')] if p),
|
||||
'sample': {
|
||||
'given_name': pgettext_lazy('person_name_sample', 'John'),
|
||||
'family_name': pgettext_lazy('person_name_sample', 'Doe'),
|
||||
},
|
||||
}),
|
||||
('title_given_family', {
|
||||
'fields': (
|
||||
('title', pgettext_lazy('person_name', 'Title'), 1),
|
||||
('given_name', _('Given name'), 2),
|
||||
('family_name', _('Family name'), 2),
|
||||
),
|
||||
'concatenation': lambda d: ' '.join(
|
||||
str(p) for p in [d.get('title', ''), d.get('given_name', ''), d.get('family_name', '')] if p
|
||||
),
|
||||
'sample': {
|
||||
'title': pgettext_lazy('person_name_sample', 'Dr'),
|
||||
'given_name': pgettext_lazy('person_name_sample', 'John'),
|
||||
'family_name': pgettext_lazy('person_name_sample', 'Doe'),
|
||||
},
|
||||
}),
|
||||
('given_middle_family', {
|
||||
'fields': (
|
||||
('given_name', _('First name'), 2),
|
||||
('middle_name', _('Middle name'), 1),
|
||||
('family_name', _('Family name'), 2),
|
||||
),
|
||||
'concatenation': lambda d: ' '.join(
|
||||
str(p) for p in [d.get('given_name', ''), d.get('middle_name', ''), d.get('family_name', '')] if p
|
||||
),
|
||||
'sample': {
|
||||
'given_name': pgettext_lazy('person_name_sample', 'John'),
|
||||
'middle_name': 'M',
|
||||
'family_name': pgettext_lazy('person_name_sample', 'Doe'),
|
||||
},
|
||||
}),
|
||||
('title_given_middle_family', {
|
||||
'fields': (
|
||||
('title', pgettext_lazy('person_name', 'Title'), 1),
|
||||
('given_name', _('First name'), 2),
|
||||
('middle_name', _('Middle name'), 1),
|
||||
('family_name', _('Family name'), 1),
|
||||
),
|
||||
'concatenation': lambda d: ' '.join(
|
||||
str(p) for p in [d.get('title', ''), d.get('given_name'), d.get('middle_name'), d.get('family_name')] if p
|
||||
),
|
||||
'sample': {
|
||||
'title': pgettext_lazy('person_name_sample', 'Dr'),
|
||||
'given_name': pgettext_lazy('person_name_sample', 'John'),
|
||||
'middle_name': 'M',
|
||||
'family_name': pgettext_lazy('person_name_sample', 'Doe'),
|
||||
},
|
||||
}),
|
||||
('family_given', {
|
||||
'fields': (
|
||||
('family_name', _('Family name'), 1),
|
||||
('given_name', _('Given name'), 1),
|
||||
),
|
||||
'concatenation': lambda d: ' '.join(
|
||||
str(p) for p in [d.get('family_name', ''), d.get('given_name', '')] if p
|
||||
),
|
||||
'sample': {
|
||||
'given_name': pgettext_lazy('person_name_sample', 'John'),
|
||||
'family_name': pgettext_lazy('person_name_sample', 'Doe'),
|
||||
},
|
||||
}),
|
||||
('family_nospace_given', {
|
||||
'fields': (
|
||||
('given_name', _('Given name'), 1),
|
||||
('family_name', _('Family name'), 1),
|
||||
),
|
||||
'concatenation': lambda d: ''.join(
|
||||
str(p) for p in [d.get('family_name', ''), d.get('given_name', '')] if p
|
||||
),
|
||||
'sample': {
|
||||
'given_name': '泽东',
|
||||
'family_name': '毛',
|
||||
},
|
||||
}),
|
||||
('family_comma_given', {
|
||||
'fields': (
|
||||
('given_name', _('Given name'), 1),
|
||||
('family_name', _('Family name'), 1),
|
||||
),
|
||||
'concatenation': lambda d: (
|
||||
str(d.get('family_name', '')) +
|
||||
str((', ' if d.get('family_name') and d.get('given_name') else '')) +
|
||||
str(d.get('given_name', ''))
|
||||
),
|
||||
'sample': {
|
||||
'given_name': pgettext_lazy('person_name_sample', 'John'),
|
||||
'family_name': pgettext_lazy('person_name_sample', 'Doe'),
|
||||
},
|
||||
}),
|
||||
('full', {
|
||||
'fields': (
|
||||
('full_name', _('Name'), 1),
|
||||
),
|
||||
'concatenation': lambda d: str(d.get('full_name', '')),
|
||||
'sample': {
|
||||
'full_name': pgettext_lazy('person_name_sample', 'John Doe'),
|
||||
},
|
||||
}),
|
||||
('calling_full', {
|
||||
'fields': (
|
||||
('calling_name', _('Calling name'), 1),
|
||||
('full_name', _('Full name'), 2),
|
||||
),
|
||||
'concatenation': lambda d: str(d.get('full_name', '')),
|
||||
'sample': {
|
||||
'full_name': pgettext_lazy('person_name_sample', 'John Doe'),
|
||||
'calling_name': pgettext_lazy('person_name_sample', 'John'),
|
||||
},
|
||||
}),
|
||||
('full_transcription', {
|
||||
'fields': (
|
||||
('full_name', _('Full name'), 1),
|
||||
('latin_transcription', _('Latin transcription'), 2),
|
||||
),
|
||||
'concatenation': lambda d: str(d.get('full_name', '')),
|
||||
'sample': {
|
||||
'full_name': '庄司',
|
||||
'latin_transcription': 'Shōji'
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
|
||||
settings_hierarkey = Hierarkey(attribute_name='settings')
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from datetime import timedelta
|
||||
from typing import List, Tuple
|
||||
|
||||
from django.db import transaction
|
||||
from django.db.models import Max
|
||||
from django.db.models import Max, Q
|
||||
from django.db.models.functions import Greatest
|
||||
from django.dispatch import receiver
|
||||
from django.utils.timezone import now
|
||||
@@ -202,12 +202,20 @@ class AttendeeNameShredder(BaseDataShredder):
|
||||
def generate_files(self) -> List[Tuple[str, str, str]]:
|
||||
yield 'attendee-names.json', 'application/json', json.dumps({
|
||||
'{}-{}'.format(op.order.code, op.positionid): op.attendee_name
|
||||
for op in OrderPosition.objects.filter(order__event=self.event, attendee_name__isnull=False)
|
||||
for op in OrderPosition.objects.filter(
|
||||
order__event=self.event
|
||||
).filter(
|
||||
Q(Q(attendee_name_cached__isnull=False) | Q(attendee_name_parts__isnull=False))
|
||||
)
|
||||
}, indent=4)
|
||||
|
||||
@transaction.atomic
|
||||
def shred_data(self):
|
||||
OrderPosition.objects.filter(order__event=self.event, attendee_name__isnull=False).update(attendee_name=None)
|
||||
OrderPosition.objects.filter(
|
||||
order__event=self.event
|
||||
).filter(
|
||||
Q(Q(attendee_name_cached__isnull=False) | Q(attendee_name_parts__isnull=False))
|
||||
).update(attendee_name_cached=None, attendee_name_parts={'_shredded': True})
|
||||
|
||||
for le in self.event.logentry_set.filter(action_type="pretix.event.order.modified").exclude(data=""):
|
||||
d = le.parsed_data
|
||||
@@ -215,6 +223,10 @@ class AttendeeNameShredder(BaseDataShredder):
|
||||
for i, row in enumerate(d['data']):
|
||||
if 'attendee_name' in row:
|
||||
d['data'][i]['attendee_name'] = '█'
|
||||
if 'attendee_name_parts' in row:
|
||||
d['data'][i]['attendee_name_parts'] = {
|
||||
'_legacy': '█'
|
||||
}
|
||||
le.data = json.dumps(d)
|
||||
le.shredded = True
|
||||
le.save(update_fields=['data', 'shredded'])
|
||||
|
||||
@@ -80,8 +80,8 @@ class BaseQuestionsViewMixin:
|
||||
# This form was correctly filled, so we store the data as
|
||||
# answers to the questions / in the CartPosition object
|
||||
for k, v in form.cleaned_data.items():
|
||||
if k == 'attendee_name':
|
||||
form.pos.attendee_name = v if v != '' else None
|
||||
if k == 'attendee_name_parts':
|
||||
form.pos.attendee_name_parts = v if v else None
|
||||
form.pos.save()
|
||||
elif k == 'attendee_email':
|
||||
form.pos.attendee_email = v if v != '' else None
|
||||
|
||||
@@ -20,6 +20,7 @@ from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
|
||||
from pretix.base.models import Event, Organizer, TaxRule
|
||||
from pretix.base.models.event import EventMetaValue, SubEvent
|
||||
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.control.forms import (
|
||||
ExtFileField, MultipleLanguagesWidget, SingleLanguageWidget, SlugWidget,
|
||||
SplitDateTimeField, SplitDateTimePickerWidget,
|
||||
@@ -338,6 +339,12 @@ class EventSettingsForm(SettingsForm):
|
||||
required=False,
|
||||
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_names_asked'}),
|
||||
)
|
||||
name_scheme = forms.ChoiceField(
|
||||
label=_("Name format"),
|
||||
help_text=_("This defines how pretix will ask for human names. Changing this after you already received "
|
||||
"orders might lead to unexpected behaviour when sorting or changing names."),
|
||||
required=True,
|
||||
)
|
||||
attendee_emails_asked = forms.BooleanField(
|
||||
label=_("Ask for email addresses per ticket"),
|
||||
help_text=_("Normally, pretix asks for one email address per order and the order confirmation will be sent "
|
||||
@@ -419,6 +426,13 @@ class EventSettingsForm(SettingsForm):
|
||||
'e.g. I hereby confirm that I have read and agree with the event organizer\'s terms of service '
|
||||
'and agree with them.'
|
||||
)
|
||||
self.fields['name_scheme'].choices = (
|
||||
(k, _('Ask for {fields}, display like {example}').format(
|
||||
fields=' + '.join(str(vv[1]) for vv in v['fields']),
|
||||
example=v['concatenation'](v['sample'])
|
||||
))
|
||||
for k, v in PERSON_NAME_SCHEMES.items()
|
||||
)
|
||||
|
||||
|
||||
class PaymentSettingsForm(SettingsForm):
|
||||
|
||||
@@ -129,7 +129,7 @@ class OrderFilterForm(FilterForm):
|
||||
|
||||
matching_positions = OrderPosition.objects.filter(
|
||||
Q(order=OuterRef('pk')) & Q(
|
||||
Q(attendee_name__icontains=u) | Q(attendee_email__icontains=u)
|
||||
Q(attendee_name_cached__icontains=u) | Q(attendee_email__icontains=u)
|
||||
| Q(secret__istartswith=u)
|
||||
)
|
||||
).values('id')
|
||||
@@ -137,7 +137,7 @@ class OrderFilterForm(FilterForm):
|
||||
qs = qs.annotate(has_pos=Exists(matching_positions)).filter(
|
||||
code
|
||||
| Q(email__icontains=u)
|
||||
| Q(invoice_address__name__icontains=u)
|
||||
| Q(invoice_address__name_cached__icontains=u)
|
||||
| Q(invoice_address__company__icontains=u)
|
||||
| Q(pk__in=matching_invoices)
|
||||
| Q(comment__icontains=u)
|
||||
@@ -568,9 +568,9 @@ class CheckInFilterForm(FilterForm):
|
||||
'item': ('item__name', 'variation__value', 'order__code'),
|
||||
'-item': ('-item__name', '-variation__value', '-order__code'),
|
||||
'name': {'_order': F('display_name').asc(nulls_first=True),
|
||||
'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')},
|
||||
'display_name': Coalesce('attendee_name_cached', 'addon_to__attendee_name_cached')},
|
||||
'-name': {'_order': F('display_name').desc(nulls_last=True),
|
||||
'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')},
|
||||
'display_name': Coalesce('attendee_name_cached', 'addon_to__attendee_name_cached')},
|
||||
}
|
||||
|
||||
user = forms.CharField(
|
||||
@@ -615,10 +615,10 @@ class CheckInFilterForm(FilterForm):
|
||||
Q(order__code__istartswith=u)
|
||||
| Q(secret__istartswith=u)
|
||||
| Q(order__email__icontains=u)
|
||||
| Q(attendee_name__icontains=u)
|
||||
| Q(attendee_name_cached__icontains=u)
|
||||
| Q(attendee_email__icontains=u)
|
||||
| Q(voucher__code__istartswith=u)
|
||||
| Q(order__invoice_address__name__icontains=u)
|
||||
| Q(order__invoice_address__name_cached__icontains=u)
|
||||
| Q(order__invoice_address__company__icontains=u)
|
||||
)
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
{% bootstrap_field sform.max_items_per_order layout="control" %}
|
||||
{% bootstrap_field sform.attendee_names_asked layout="control" %}
|
||||
{% bootstrap_field sform.attendee_names_required layout="control" %}
|
||||
{% bootstrap_field sform.name_scheme layout="control" %}
|
||||
{% bootstrap_field sform.order_email_asked_twice layout="control" %}
|
||||
{% bootstrap_field sform.attendee_emails_asked layout="control" %}
|
||||
{% bootstrap_field sform.attendee_emails_required layout="control" %}
|
||||
|
||||
@@ -644,7 +644,7 @@ class MailSettingsRendererPreview(MailSettingsPreview):
|
||||
expires=now(), code="PREVIEW", total=119)
|
||||
item = request.event.items.create(name=ugettext("Sample product"), default_price=42.23,
|
||||
description=ugettext("Sample product description"))
|
||||
order.positions.create(item=item, attendee_name=ugettext("John Doe"), price=item.default_price)
|
||||
order.positions.create(item=item, attendee_name_parts={'full_name': ugettext("John Doe")}, price=item.default_price)
|
||||
v = renderers[request.GET.get('renderer')].render(
|
||||
v,
|
||||
str(request.event.settings.mail_text_signature),
|
||||
|
||||
@@ -18,6 +18,7 @@ from django.views.generic import TemplateView
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import CachedFile, InvoiceAddress, OrderPosition
|
||||
from pretix.base.pdf import get_variables
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.helpers.database import rolledback_transaction
|
||||
from pretix.presale.style import get_fonts
|
||||
@@ -65,11 +66,13 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
|
||||
locale=self.request.event.settings.locale,
|
||||
expires=now(), code="PREVIEW1234", total=119)
|
||||
|
||||
p = order.positions.create(item=item, attendee_name=_("John Doe"), price=item.default_price)
|
||||
order.positions.create(item=item2, attendee_name=_("John Doe"), price=item.default_price, addon_to=p)
|
||||
order.positions.create(item=item2, attendee_name=_("John Doe"), price=item.default_price, addon_to=p)
|
||||
scheme = PERSON_NAME_SCHEMES[self.request.event.settings.name_scheme]
|
||||
sample = {k: str(v) for k, v in scheme['sample'].items()}
|
||||
p = order.positions.create(item=item, attendee_name_parts=sample, price=item.default_price)
|
||||
order.positions.create(item=item2, attendee_name_parts=sample, price=item.default_price, addon_to=p)
|
||||
order.positions.create(item=item2, attendee_name_parts=sample, price=item.default_price, addon_to=p)
|
||||
|
||||
InvoiceAddress.objects.create(order=order, name=_("John Doe"), company=_("Sample company"))
|
||||
InvoiceAddress.objects.create(order=order, name_parts=sample, company=_("Sample company"))
|
||||
return p
|
||||
|
||||
def generate(self, p: OrderPosition, override_layout=None, override_background=None):
|
||||
|
||||
@@ -36,7 +36,8 @@ class OrderSearch(PaginationMixin, ListView):
|
||||
qs = self.filter_form.filter_qs(qs)
|
||||
|
||||
return qs.only(
|
||||
'id', 'invoice_address__name', 'code', 'event', 'email', 'datetime', 'total', 'status'
|
||||
'id', 'invoice_address__name_cached', 'invoice_address__name_parts', 'code', 'event', 'email',
|
||||
'datetime', 'total', 'status'
|
||||
).prefetch_related(
|
||||
'event', 'event__organizer'
|
||||
)
|
||||
|
||||
@@ -4,11 +4,14 @@ from io import BytesIO
|
||||
from typing import Tuple
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.staticfiles import finders
|
||||
from django.core.files import File
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files.storage import default_storage
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils.translation import ugettext as _
|
||||
from jsonfallback.functions import JSONExtract
|
||||
from PyPDF2 import PdfFileMerger
|
||||
from reportlab.lib import pagesizes
|
||||
from reportlab.pdfgen import canvas
|
||||
@@ -17,6 +20,7 @@ from pretix.base.exporter import BaseExporter
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import Order, OrderPosition
|
||||
from pretix.base.pdf import Renderer
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.plugins.badges.models import BadgeItem, BadgeLayout
|
||||
|
||||
|
||||
@@ -67,6 +71,7 @@ class BadgeExporter(BaseExporter):
|
||||
|
||||
@property
|
||||
def export_form_fields(self):
|
||||
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
|
||||
d = OrderedDict(
|
||||
[
|
||||
('items',
|
||||
@@ -86,10 +91,13 @@ class BadgeExporter(BaseExporter):
|
||||
('order_by',
|
||||
forms.ChoiceField(
|
||||
label=_('Sort by'),
|
||||
choices=(
|
||||
choices=[
|
||||
('name', _('Attendee name')),
|
||||
('last_name', _('Last part of attendee name')),
|
||||
)
|
||||
('code', _('Order code')),
|
||||
] + ([
|
||||
('name:{}'.format(k), _('Attendee name: {part}').format(part=label))
|
||||
for k, label, w in name_scheme['fields']
|
||||
] if settings.JSON_FIELD_AVAILABLE and len(name_scheme['fields']) > 1 else []),
|
||||
)),
|
||||
]
|
||||
)
|
||||
@@ -108,10 +116,18 @@ class BadgeExporter(BaseExporter):
|
||||
qs = qs.filter(order__status__in=[Order.STATUS_PAID])
|
||||
|
||||
if form_data.get('order_by') == 'name':
|
||||
qs = qs.order_by('attendee_name', 'order__code')
|
||||
elif form_data.get('order_by') == 'last_name':
|
||||
qs = qs.order_by('attendee_name_cached', 'order__code')
|
||||
elif form_data.get('order_by') == 'code':
|
||||
qs = qs.order_by('order__code')
|
||||
qs = sorted(qs, key=lambda op: op.attendee_name.split()[-1] if op.attendee_name else '')
|
||||
elif form_data.get('order_by', '').startswith('name:'):
|
||||
part = form_data['order_by'][5:]
|
||||
qs = qs.annotate(
|
||||
resolved_name=Coalesce('attendee_name_parts', 'addon_to__attendee_name_parts', 'order__invoice_address__name_parts')
|
||||
).annotate(
|
||||
resolved_name_part=JSONExtract('resolved_name', part)
|
||||
).order_by(
|
||||
'resolved_name_part'
|
||||
)
|
||||
|
||||
outbuffer = render_pdf(self.event, qs)
|
||||
return 'badges.pdf', 'application/pdf', outbuffer.read()
|
||||
|
||||
@@ -170,7 +170,7 @@ class ActionView(View):
|
||||
qs = self.order_qs().order_by('pk').annotate(inr=Concat('invoices__prefix', 'invoices__invoice_no')).filter(
|
||||
code
|
||||
| Q(email__icontains=u)
|
||||
| Q(positions__attendee_name__icontains=u)
|
||||
| Q(positions__attendee_name_cached__icontains=u)
|
||||
| Q(positions__attendee_email__icontains=u)
|
||||
| Q(invoice_address__name__icontains=u)
|
||||
| Q(invoice_address__company__icontains=u)
|
||||
|
||||
@@ -4,18 +4,21 @@ from collections import OrderedDict
|
||||
import dateutil.parser
|
||||
from defusedcsv import csv
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.db.models import Max, OuterRef, Subquery
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.urls import reverse
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.timezone import is_aware, make_aware
|
||||
from django.utils.translation import pgettext, ugettext as _, ugettext_lazy
|
||||
from jsonfallback.functions import JSONExtract
|
||||
from pytz import UTC
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.platypus import Flowable, Paragraph, Spacer, Table, TableStyle
|
||||
|
||||
from pretix.base.exporter import BaseExporter
|
||||
from pretix.base.models import Checkin, Order, OrderPosition, Question
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.control.forms.widgets import Select2
|
||||
from pretix.plugins.reports.exporters import ReportlabExportMixin
|
||||
@@ -24,6 +27,7 @@ from pretix.plugins.reports.exporters import ReportlabExportMixin
|
||||
class BaseCheckinList(BaseExporter):
|
||||
@property
|
||||
def export_form_fields(self):
|
||||
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
|
||||
d = OrderedDict(
|
||||
[
|
||||
('list',
|
||||
@@ -44,10 +48,13 @@ class BaseCheckinList(BaseExporter):
|
||||
forms.ChoiceField(
|
||||
label=_('Sort by'),
|
||||
initial='name',
|
||||
choices=(
|
||||
choices=[
|
||||
('name', _('Attendee name')),
|
||||
('code', _('Order code')),
|
||||
),
|
||||
] + ([
|
||||
('name:{}'.format(k), _('Attendee name: {part}').format(part=label))
|
||||
for k, label, w in name_scheme['fields']
|
||||
] if settings.JSON_FIELD_AVAILABLE and len(name_scheme['fields']) > 1 else []),
|
||||
widget=forms.RadioSelect,
|
||||
required=False
|
||||
)),
|
||||
@@ -79,6 +86,49 @@ class BaseCheckinList(BaseExporter):
|
||||
|
||||
return d
|
||||
|
||||
def _get_queryset(self, cl, form_data):
|
||||
cqs = Checkin.objects.filter(
|
||||
position_id=OuterRef('pk'),
|
||||
list_id=cl.pk
|
||||
).order_by().values('position_id').annotate(
|
||||
m=Max('datetime')
|
||||
).values('m')
|
||||
qs = OrderPosition.objects.filter(
|
||||
order__event=self.event,
|
||||
).annotate(
|
||||
last_checked_in=Subquery(cqs)
|
||||
).prefetch_related(
|
||||
'answers', 'answers__question', 'addon_to__answers', 'addon_to__answers__question'
|
||||
).select_related('order', 'item', 'variation', 'addon_to', 'order__invoice_address')
|
||||
|
||||
if not cl.all_products:
|
||||
qs = qs.filter(item__in=cl.limit_products.values_list('id', flat=True))
|
||||
|
||||
if cl.subevent:
|
||||
qs = qs.filter(subevent=cl.subevent)
|
||||
|
||||
if form_data['sort'] == 'name':
|
||||
qs = qs.order_by(Coalesce('attendee_name_cached', 'addon_to__attendee_name_cached', 'order__invoice_address__name_cached'),
|
||||
'order__code')
|
||||
elif form_data['sort'] == 'code':
|
||||
qs = qs.order_by('order__code')
|
||||
elif form_data['sort'].startswith('name:'):
|
||||
part = form_data['sort'][5:]
|
||||
qs = qs.annotate(
|
||||
resolved_name=Coalesce('attendee_name_parts', 'addon_to__attendee_name_parts', 'order__invoice_address__name_parts')
|
||||
).annotate(
|
||||
resolved_name_part=JSONExtract('resolved_name', part)
|
||||
).order_by(
|
||||
'resolved_name_part'
|
||||
)
|
||||
|
||||
if not cl.include_pending:
|
||||
qs = qs.filter(order__status=Order.STATUS_PAID)
|
||||
else:
|
||||
qs = qs.filter(order__status__in=(Order.STATUS_PAID, Order.STATUS_PENDING))
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
class CBFlowable(Flowable):
|
||||
def __init__(self, checked=False):
|
||||
@@ -174,37 +224,7 @@ class PDFCheckinList(ReportlabExportMixin, BaseCheckinList):
|
||||
p = Paragraph(txt, headrowstyle)
|
||||
tdata[0].append(p)
|
||||
|
||||
cqs = Checkin.objects.filter(
|
||||
position_id=OuterRef('pk'),
|
||||
list_id=cl.pk
|
||||
).order_by().values('position_id').annotate(
|
||||
m=Max('datetime')
|
||||
).values('m')
|
||||
|
||||
qs = OrderPosition.objects.filter(
|
||||
order__event=self.event,
|
||||
).annotate(
|
||||
last_checked_in=Subquery(cqs)
|
||||
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address').prefetch_related(
|
||||
'answers', 'answers__question', 'addon_to__answers', 'addon_to__answers__question'
|
||||
)
|
||||
|
||||
if not cl.all_products:
|
||||
qs = qs.filter(item__in=cl.limit_products.values_list('id', flat=True))
|
||||
|
||||
if cl.subevent:
|
||||
qs = qs.filter(subevent=cl.subevent)
|
||||
|
||||
if form_data['sort'] == 'name':
|
||||
qs = qs.order_by(Coalesce('attendee_name', 'addon_to__attendee_name', 'order__invoice_address__name'),
|
||||
'order__code')
|
||||
elif form_data['sort'] == 'code':
|
||||
qs = qs.order_by('order__code')
|
||||
|
||||
if not cl.include_pending:
|
||||
qs = qs.filter(order__status=Order.STATUS_PAID)
|
||||
else:
|
||||
qs = qs.filter(order__status__in=(Order.STATUS_PAID, Order.STATUS_PENDING))
|
||||
qs = self._get_queryset(cl, form_data)
|
||||
|
||||
for op in qs:
|
||||
try:
|
||||
@@ -216,7 +236,7 @@ class PDFCheckinList(ReportlabExportMixin, BaseCheckinList):
|
||||
|
||||
name = op.attendee_name or (op.addon_to.attendee_name if op.addon_to else '') or ian
|
||||
if iac:
|
||||
name += "\n" + iac
|
||||
name += "<br/>" + iac
|
||||
|
||||
row = [
|
||||
'!!' if op.item.checkin_attention else '',
|
||||
@@ -282,33 +302,18 @@ class CSVCheckinList(BaseCheckinList):
|
||||
|
||||
questions = list(Question.objects.filter(event=self.event, id__in=form_data['questions']))
|
||||
|
||||
cqs = Checkin.objects.filter(
|
||||
position_id=OuterRef('pk'),
|
||||
list_id=cl.pk
|
||||
).order_by().values('position_id').annotate(
|
||||
m=Max('datetime')
|
||||
).values('m')
|
||||
qs = OrderPosition.objects.filter(
|
||||
order__event=self.event,
|
||||
).annotate(
|
||||
last_checked_in=Subquery(cqs)
|
||||
).prefetch_related(
|
||||
'answers', 'answers__question', 'addon_to__answers', 'addon_to__answers__question'
|
||||
).select_related('order', 'item', 'variation', 'addon_to')
|
||||
|
||||
if not cl.all_products:
|
||||
qs = qs.filter(item__in=cl.limit_products.values_list('id', flat=True))
|
||||
|
||||
if cl.subevent:
|
||||
qs = qs.filter(subevent=cl.subevent)
|
||||
|
||||
if form_data['sort'] == 'name':
|
||||
qs = qs.order_by(Coalesce('attendee_name', 'addon_to__attendee_name'))
|
||||
elif form_data['sort'] == 'code':
|
||||
qs = qs.order_by('order__code')
|
||||
qs = self._get_queryset(cl, form_data)
|
||||
|
||||
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
|
||||
headers = [
|
||||
_('Order code'), _('Attendee name'), _('Product'), _('Price'), _('Checked in')
|
||||
_('Order code'),
|
||||
_('Attendee name'),
|
||||
]
|
||||
if len(name_scheme['fields']) > 1:
|
||||
for k, label, w in name_scheme['fields']:
|
||||
headers.append(_('Attendee name: {part}').format(part=label))
|
||||
headers += [
|
||||
_('Product'), _('Price'), _('Checked in')
|
||||
]
|
||||
if not cl.include_pending:
|
||||
qs = qs.filter(order__status=Order.STATUS_PAID)
|
||||
@@ -340,6 +345,13 @@ class CSVCheckinList(BaseCheckinList):
|
||||
row = [
|
||||
op.order.code,
|
||||
op.attendee_name or (op.addon_to.attendee_name if op.addon_to else ''),
|
||||
]
|
||||
if len(name_scheme['fields']) > 1:
|
||||
for k, label, w in name_scheme['fields']:
|
||||
row.append(
|
||||
(op.attendee_name_parts or (op.addon_to.attendee_name_parts if op.addon_to else {})).get(k, '')
|
||||
)
|
||||
row += [
|
||||
str(op.item) + (" – " + str(op.variation.value) if op.variation else ""),
|
||||
op.price,
|
||||
date_format(last_checked_in.astimezone(self.event.timezone), 'SHORT_DATETIME_FORMAT')
|
||||
|
||||
@@ -302,10 +302,10 @@ class ApiSearchView(ApiView):
|
||||
else:
|
||||
ops = qs.filter(
|
||||
Q(secret__istartswith=query)
|
||||
| Q(attendee_name__icontains=query)
|
||||
| Q(addon_to__attendee_name__icontains=query)
|
||||
| Q(attendee_name_cached__icontains=query)
|
||||
| Q(addon_to__attendee_name_cached__icontains=query)
|
||||
| Q(order__code__istartswith=query)
|
||||
| Q(order__invoice_address__name__icontains=query)
|
||||
| Q(order__invoice_address__name_cached__icontains=query)
|
||||
)[:25]
|
||||
|
||||
response['results'] = [serialize_op(op, bool(op.last_checked_in), self.config.list) for op in ops]
|
||||
|
||||
@@ -1,28 +1,80 @@
|
||||
from collections import OrderedDict
|
||||
from io import BytesIO
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils.translation import ugettext as _
|
||||
from jsonfallback.functions import JSONExtract
|
||||
from PyPDF2.merger import PdfFileMerger
|
||||
|
||||
from pretix.base.exporter import BaseExporter
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import Order, OrderPosition
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
|
||||
from .ticketoutput import PdfTicketOutput
|
||||
|
||||
|
||||
class AllTicketsPDF(BaseExporter):
|
||||
name = "alltickets"
|
||||
verbose_name = _("All paid PDF tickets in one file")
|
||||
verbose_name = _("All PDF tickets in one file")
|
||||
identifier = "pdfoutput_all_tickets"
|
||||
|
||||
@property
|
||||
def export_form_fields(self):
|
||||
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
|
||||
d = OrderedDict(
|
||||
[
|
||||
('include_pending',
|
||||
forms.BooleanField(
|
||||
label=_('Include pending orders'),
|
||||
required=False
|
||||
)),
|
||||
('order_by',
|
||||
forms.ChoiceField(
|
||||
label=_('Sort by'),
|
||||
choices=[
|
||||
('name', _('Attendee name')),
|
||||
('code', _('Order code')),
|
||||
] + ([
|
||||
('name:{}'.format(k), _('Attendee name: {part}').format(part=label))
|
||||
for k, label, w in name_scheme['fields']
|
||||
] if settings.JSON_FIELD_AVAILABLE and len(name_scheme['fields']) > 1 else []),
|
||||
)),
|
||||
]
|
||||
)
|
||||
return d
|
||||
|
||||
def render(self, form_data):
|
||||
merger = PdfFileMerger()
|
||||
|
||||
o = PdfTicketOutput(self.event)
|
||||
qs = OrderPosition.objects.filter(order__event=self.event, order__status=Order.STATUS_PAID).select_related(
|
||||
'order', 'item', 'variation'
|
||||
)
|
||||
qs = OrderPosition.objects.filter(
|
||||
order__event=self.event
|
||||
).prefetch_related(
|
||||
'answers', 'answers__question'
|
||||
).select_related('order', 'item', 'variation', 'addon_to')
|
||||
|
||||
if form_data.get('include_pending'):
|
||||
qs = qs.filter(order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING])
|
||||
else:
|
||||
qs = qs.filter(order__status__in=[Order.STATUS_PAID])
|
||||
|
||||
if form_data.get('order_by') == 'name':
|
||||
qs = qs.order_by('attendee_name_cached', 'order__code')
|
||||
elif form_data.get('order_by') == 'code':
|
||||
qs = qs.order_by('order__code')
|
||||
elif form_data.get('order_by', '').startswith('name:'):
|
||||
part = form_data['order_by'][5:]
|
||||
qs = qs.annotate(
|
||||
resolved_name=Coalesce('attendee_name_parts', 'addon_to__attendee_name_parts', 'order__invoice_address__name_parts')
|
||||
).annotate(
|
||||
resolved_name_part=JSONExtract('resolved_name', part)
|
||||
).order_by(
|
||||
'resolved_name_part'
|
||||
)
|
||||
|
||||
for op in qs:
|
||||
if op.addon_to_id and not self.event.settings.ticket_download_addons:
|
||||
|
||||
@@ -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"}]'),
|
||||
),
|
||||
]
|
||||
@@ -391,7 +391,7 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
messages.warning(request, _('Please fill in answers to all required questions.'))
|
||||
return False
|
||||
if cp.item.admission and self.request.event.settings.get('attendee_names_required', as_type=bool) \
|
||||
and cp.attendee_name is None:
|
||||
and not cp.attendee_name_parts:
|
||||
if warn:
|
||||
messages.warning(request, _('Please fill in answers to all required questions.'))
|
||||
return False
|
||||
|
||||
@@ -66,7 +66,7 @@ class InvoiceNameForm(InvoiceAddressForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
for f in list(self.fields.keys()):
|
||||
if f != 'name':
|
||||
if f != 'name_parts':
|
||||
del self.fields[f]
|
||||
|
||||
|
||||
|
||||
@@ -34,7 +34,8 @@
|
||||
{% endcompress %}
|
||||
<meta name="referrer" content="origin">
|
||||
{{ 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 %}
|
||||
<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" %}">
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
|
||||
{% if frontpage_text and not cart_namespace %}
|
||||
<div>
|
||||
{{ frontpage_text|rich_text }}
|
||||
{{ frontpage_text|rich_text|linebreaksbr }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -184,6 +184,8 @@ def get_cart(request):
|
||||
'item', 'variation', 'subevent', 'subevent__event', 'subevent__event__organizer',
|
||||
'item__tax_rule'
|
||||
)
|
||||
for cp in request._cart_cache:
|
||||
cp.event = request.event # Populate field with known value to save queries
|
||||
return request._cart_cache
|
||||
|
||||
|
||||
|
||||
@@ -58,6 +58,8 @@ debug_fallback = "runserver" in sys.argv
|
||||
DEBUG = config.getboolean('django', 'debug', fallback=debug_fallback)
|
||||
|
||||
db_backend = config.get('database', 'backend', fallback='sqlite3')
|
||||
if db_backend == 'postgresql_psycopg2':
|
||||
db_backend = 'postgresql'
|
||||
DATABASE_IS_GALERA = config.getboolean('database', 'galera', fallback=False)
|
||||
if DATABASE_IS_GALERA and 'mysql' in db_backend:
|
||||
db_options = {
|
||||
@@ -66,6 +68,10 @@ if DATABASE_IS_GALERA and 'mysql' in db_backend:
|
||||
else:
|
||||
db_options = {}
|
||||
|
||||
if 'mysql' in db_backend:
|
||||
db_options['charset'] = 'utf8mb4'
|
||||
JSON_FIELD_AVAILABLE = db_backend in ('mysql', 'postgresql')
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.' + db_backend,
|
||||
@@ -75,7 +81,11 @@ DATABASES = {
|
||||
'HOST': config.get('database', 'host', fallback=''),
|
||||
'PORT': config.get('database', 'port', fallback=''),
|
||||
'CONN_MAX_AGE': 0 if db_backend == 'sqlite3' else 120,
|
||||
'OPTIONS': db_options
|
||||
'OPTIONS': db_options,
|
||||
'TEST': {
|
||||
'CHARSET': 'utf8mb4',
|
||||
'COLLATION': 'utf8mb4_unicode_ci',
|
||||
} if 'mysql' in db_backend else {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -366,3 +366,52 @@ table td > .checkbox input[type="checkbox"] {
|
||||
box-shadow: 0 1px 3px 0 #aaa;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $screen-xs-max) {
|
||||
.nameparts-form-group {
|
||||
display: block;
|
||||
input:not(:first-child) {
|
||||
border-top-right-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
input:not(:last-child) {
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media(min-width: $screen-sm-min) {
|
||||
.nameparts-form-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
input {
|
||||
width: auto;
|
||||
flex-basis: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
input:not(:first-child) {
|
||||
border-bottom-left-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
input:not(:last-child) {
|
||||
border-bottom-right-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
input[data-size="1"] {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 4;
|
||||
}
|
||||
input[data-size="2"] {
|
||||
flex-grow: 2;
|
||||
flex-shrink: 3;
|
||||
}
|
||||
input[data-size="3"] {
|
||||
flex-grow: 3;
|
||||
flex-shrink: 2;
|
||||
}
|
||||
input[data-size="4"] {
|
||||
flex-grow: 4;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,7 +189,10 @@ $(function () {
|
||||
dependency = $($(this).attr("data-required-if")),
|
||||
update = function (ev) {
|
||||
var enabled = (dependency.attr("type") === 'checkbox' || dependency.attr("type") === 'radio') ? dependency.prop('checked') : !!dependency.val();
|
||||
dependent.prop('required', enabled).closest('.form-group').toggleClass('required', enabled);
|
||||
if (!dependent.is("[data-no-required-attr]")) {
|
||||
dependent.prop('required', enabled);
|
||||
}
|
||||
dependent.closest('.form-group').toggleClass('required', enabled);
|
||||
};
|
||||
update();
|
||||
dependency.closest('.form-group').find('input[name=' + dependency.attr("name") + ']').on("change", update);
|
||||
|
||||
@@ -92,3 +92,52 @@
|
||||
border-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $screen-xs-max) {
|
||||
.nameparts-form-group {
|
||||
display: block;
|
||||
input:not(:first-child) {
|
||||
border-top-right-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
input:not(:last-child) {
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media(min-width: $screen-sm-min) {
|
||||
.nameparts-form-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
input {
|
||||
width: auto;
|
||||
flex-basis: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
input:not(:first-child) {
|
||||
border-bottom-left-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
input:not(:last-child) {
|
||||
border-bottom-right-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
input[data-size="1"] {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 4;
|
||||
}
|
||||
input[data-size="2"] {
|
||||
flex-grow: 2;
|
||||
flex-shrink: 3;
|
||||
}
|
||||
input[data-size="3"] {
|
||||
flex-grow: 3;
|
||||
flex-shrink: 2;
|
||||
}
|
||||
input[data-size="4"] {
|
||||
flex-grow: 4;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
psycopg2-binary
|
||||
|
||||
@@ -34,6 +34,8 @@ babel
|
||||
django-i18nfield>=1.4.0
|
||||
django-hijack>=2.1.10,<2.2.0
|
||||
django-oauth-toolkit==1.2.*
|
||||
django-jsonfallback
|
||||
psycopg2-binary
|
||||
# Stripe
|
||||
stripe==2.0.*
|
||||
# PayPal
|
||||
|
||||
@@ -13,7 +13,7 @@ known_third_party = versions
|
||||
known_standard_library = typing,enum,mimetypes
|
||||
multi_line_output = 5
|
||||
not_skip = __init__.py
|
||||
skip = make_testdata.py,wsgi.py,bootstrap,celery_app.py,settings.py
|
||||
skip = make_testdata.py,wsgi.py,bootstrap,celery_app.py,pretix/settings.py,tests/settings.py,pretix/testutils/settings.py
|
||||
|
||||
[tool:pytest]
|
||||
DJANGO_SETTINGS_MODULE=tests.settings
|
||||
|
||||
@@ -127,6 +127,8 @@ setup(
|
||||
'chardet<3.1.0,>=3.0.2',
|
||||
'mt-940==3.2',
|
||||
'django-i18nfield>=1.4.0',
|
||||
'django-jsonfallback',
|
||||
'psycopg2-binary',
|
||||
'vobject==0.9.*',
|
||||
'pycountry',
|
||||
'django-countries',
|
||||
@@ -157,7 +159,6 @@ setup(
|
||||
],
|
||||
'memcached': ['pylibmc'],
|
||||
'mysql': ['mysqlclient'],
|
||||
'postgres': ['psycopg2-binary'],
|
||||
},
|
||||
|
||||
packages=find_packages(exclude=['tests', 'tests.*']),
|
||||
|
||||
@@ -54,7 +54,8 @@ TEST_CARTPOSITION_RES = {
|
||||
'item': 1,
|
||||
'variation': None,
|
||||
'price': '23.00',
|
||||
'attendee_name': None,
|
||||
'attendee_name_parts': {'full_name': 'Peter'},
|
||||
'attendee_name': 'Peter',
|
||||
'attendee_email': None,
|
||||
'voucher': None,
|
||||
'addon_to': None,
|
||||
@@ -74,7 +75,7 @@ def test_cp_list(token_client, organizer, event, item, taxrule, question):
|
||||
mock_now.return_value = testtime
|
||||
cr = CartPosition.objects.create(
|
||||
event=event, cart_id="aaa", item=item,
|
||||
price=23,
|
||||
price=23, attendee_name_parts={'full_name': 'Peter'},
|
||||
datetime=datetime.datetime(2018, 6, 11, 10, 0, 0, 0),
|
||||
expires=datetime.datetime(2018, 6, 11, 10, 0, 0, 0)
|
||||
)
|
||||
@@ -95,7 +96,7 @@ def test_cp_list_api(token_client, organizer, event, item, taxrule, question):
|
||||
mock_now.return_value = testtime
|
||||
cr = CartPosition.objects.create(
|
||||
event=event, cart_id="aaa@api", item=item,
|
||||
price=23,
|
||||
price=23, attendee_name_parts={'full_name': 'Peter'},
|
||||
datetime=datetime.datetime(2018, 6, 11, 10, 0, 0, 0),
|
||||
expires=datetime.datetime(2018, 6, 11, 10, 0, 0, 0)
|
||||
)
|
||||
@@ -116,7 +117,7 @@ def test_cp_detail(token_client, organizer, event, item, taxrule, question):
|
||||
mock_now.return_value = testtime
|
||||
cr = CartPosition.objects.create(
|
||||
event=event, cart_id="aaa@api", item=item,
|
||||
price=23,
|
||||
price=23, attendee_name_parts={'full_name': 'Peter'},
|
||||
datetime=datetime.datetime(2018, 6, 11, 10, 0, 0, 0),
|
||||
expires=datetime.datetime(2018, 6, 11, 10, 0, 0, 0)
|
||||
)
|
||||
@@ -137,7 +138,7 @@ def test_cp_delete(token_client, organizer, event, item, taxrule, question):
|
||||
mock_now.return_value = testtime
|
||||
cr = CartPosition.objects.create(
|
||||
event=event, cart_id="aaa@api", item=item,
|
||||
price=23,
|
||||
price=23, attendee_name_parts={'full_name': 'Peter'},
|
||||
datetime=datetime.datetime(2018, 6, 11, 10, 0, 0, 0),
|
||||
expires=datetime.datetime(2018, 6, 11, 10, 0, 0, 0)
|
||||
)
|
||||
@@ -154,7 +155,7 @@ CARTPOS_CREATE_PAYLOAD = {
|
||||
'item': 1,
|
||||
'variation': None,
|
||||
'price': '23.00',
|
||||
'attendee_name': None,
|
||||
'attendee_name_parts': {'full_name': 'Peter'},
|
||||
'attendee_email': None,
|
||||
'addon_to': None,
|
||||
'subevent': None,
|
||||
@@ -177,6 +178,31 @@ def test_cartpos_create(token_client, organizer, event, item, quota, question):
|
||||
cp = CartPosition.objects.get(pk=resp.data['id'])
|
||||
assert cp.price == Decimal('23.00')
|
||||
assert cp.item == item
|
||||
assert cp.attendee_name_parts == {'full_name': 'Peter'}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cartpos_create_legacy_name(token_client, organizer, event, item, quota, question):
|
||||
res = copy.deepcopy(CARTPOS_CREATE_PAYLOAD)
|
||||
res['item'] = item.pk
|
||||
res['attendee_name'] = 'Peter'
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
del res['attendee_name_parts']
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
cp = CartPosition.objects.get(pk=resp.data['id'])
|
||||
assert cp.price == Decimal('23.00')
|
||||
assert cp.item == item
|
||||
assert cp.attendee_name_parts == {'_legacy': 'Peter'}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
||||
@@ -50,7 +50,7 @@ def order(event, item, other_item, taxrule):
|
||||
item=item,
|
||||
variation=None,
|
||||
price=Decimal("23"),
|
||||
attendee_name="Peter",
|
||||
attendee_name_parts={'full_name': "Peter"},
|
||||
secret="z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||
pseudonymization_id="ABCDEFGHKL",
|
||||
)
|
||||
@@ -60,7 +60,7 @@ def order(event, item, other_item, taxrule):
|
||||
item=other_item,
|
||||
variation=None,
|
||||
price=Decimal("23"),
|
||||
attendee_name="Michael",
|
||||
attendee_name_parts={'full_name': "Michael"},
|
||||
secret="sf4HZG73fU6kwddgjg2QOusFbYZwVKpK",
|
||||
pseudonymization_id="BACDEFGHKL",
|
||||
)
|
||||
@@ -75,6 +75,7 @@ TEST_ORDERPOSITION1_RES = {
|
||||
"variation": None,
|
||||
"price": "23.00",
|
||||
"attendee_name": "Peter",
|
||||
"attendee_name_parts": {'full_name': "Peter"},
|
||||
"attendee_email": None,
|
||||
"voucher": None,
|
||||
"tax_rate": "0.00",
|
||||
@@ -97,6 +98,7 @@ TEST_ORDERPOSITION2_RES = {
|
||||
"variation": None,
|
||||
"price": "23.00",
|
||||
"attendee_name": "Michael",
|
||||
"attendee_name_parts": {'full_name': "Michael"},
|
||||
"attendee_email": None,
|
||||
"voucher": None,
|
||||
"tax_rate": "0.00",
|
||||
|
||||
@@ -49,7 +49,7 @@ def order_position(item, order, taxrule, variations):
|
||||
tax_rate=taxrule.rate,
|
||||
tax_value=Decimal("3"),
|
||||
price=Decimal("23"),
|
||||
attendee_name="Peter",
|
||||
attendee_name_parts={'full_name': "Peter"},
|
||||
secret="z3fsn8jyufm5kpk768q69gkbyr5f4h6w"
|
||||
)
|
||||
return op
|
||||
|
||||
@@ -61,7 +61,7 @@ def order_position(item, order, taxrule, variations):
|
||||
tax_rate=taxrule.rate,
|
||||
tax_value=Decimal("3"),
|
||||
price=Decimal("23"),
|
||||
attendee_name="Peter",
|
||||
attendee_name_parts={'full_name': "Peter"},
|
||||
secret="z3fsn8jyufm5kpk768q69gkbyr5f4h6w"
|
||||
)
|
||||
return op
|
||||
|
||||
@@ -100,7 +100,7 @@ def order(event, item, taxrule, question):
|
||||
item=item,
|
||||
variation=None,
|
||||
price=Decimal("23"),
|
||||
attendee_name="Peter",
|
||||
attendee_name_parts={"full_name": "Peter", "_scheme": "full"},
|
||||
secret="z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||
pseudonymization_id="ABCDEFGHKL",
|
||||
)
|
||||
@@ -115,6 +115,7 @@ TEST_ORDERPOSITION_RES = {
|
||||
"item": 1,
|
||||
"variation": None,
|
||||
"price": "23.00",
|
||||
"attendee_name_parts": {"full_name": "Peter", "_scheme": "full"},
|
||||
"attendee_name": "Peter",
|
||||
"attendee_email": None,
|
||||
"voucher": None,
|
||||
@@ -195,6 +196,7 @@ TEST_ORDER_RES = {
|
||||
"is_business": False,
|
||||
"company": "Sample company",
|
||||
"name": "",
|
||||
"name_parts": {},
|
||||
"street": "",
|
||||
"zipcode": "",
|
||||
"city": "",
|
||||
@@ -703,7 +705,7 @@ def test_orderposition_delete(token_client, organizer, event, order, item, quest
|
||||
item=item,
|
||||
variation=None,
|
||||
price=Decimal("23"),
|
||||
attendee_name="Peter",
|
||||
attendee_name_parts={"full_name": "Peter", "_scheme": "full"},
|
||||
secret="foobar",
|
||||
pseudonymization_id="BAZ",
|
||||
)
|
||||
@@ -1249,7 +1251,7 @@ ORDER_CREATE_PAYLOAD = {
|
||||
"invoice_address": {
|
||||
"is_business": False,
|
||||
"company": "Sample company",
|
||||
"name": "Fo",
|
||||
"name_parts": {"full_name": "Fo"},
|
||||
"street": "Bar",
|
||||
"zipcode": "",
|
||||
"city": "Sample City",
|
||||
@@ -1263,7 +1265,7 @@ ORDER_CREATE_PAYLOAD = {
|
||||
"item": 1,
|
||||
"variation": None,
|
||||
"price": "23.00",
|
||||
"attendee_name": "Peter",
|
||||
"attendee_name_parts": {"full_name": "Peter"},
|
||||
"attendee_email": None,
|
||||
"addon_to": None,
|
||||
"answers": [
|
||||
@@ -1306,10 +1308,13 @@ def test_order_create(token_client, organizer, event, item, quota, question):
|
||||
assert fee.value == Decimal('0.25')
|
||||
ia = o.invoice_address
|
||||
assert ia.company == "Sample company"
|
||||
assert ia.name_parts == {"full_name": "Fo", "_scheme": "full"}
|
||||
assert ia.name_cached == "Fo"
|
||||
assert o.positions.count() == 1
|
||||
pos = o.positions.first()
|
||||
assert pos.item == item
|
||||
assert pos.price == Decimal("23.00")
|
||||
assert pos.attendee_name_parts == {"full_name": "Peter", "_scheme": "full"}
|
||||
answ = pos.answers.first()
|
||||
assert answ.question == question
|
||||
assert answ.answer == "S"
|
||||
@@ -1332,6 +1337,54 @@ def test_order_create_invoice_address_optional(token_client, organizer, event, i
|
||||
o.invoice_address
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_legacy_attendee_name(token_client, organizer, event, item, quota, question):
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['positions'][0]['attendee_name'] = 'Peter'
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
del res['positions'][0]['attendee_name_parts']
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
o = Order.objects.get(code=resp.data['code'])
|
||||
assert o.positions.first().attendee_name_parts == {"_legacy": "Peter"}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_legacy_invoice_name(token_client, organizer, event, item, quota, question):
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['invoice_address']['name'] = 'Peter'
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
del res['invoice_address']['name_parts']
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
o = Order.objects.get(code=resp.data['code'])
|
||||
assert o.invoice_address.name_parts == {"_legacy": "Peter"}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_code_optional(token_client, organizer, event, item, quota, question):
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
@@ -1646,7 +1699,7 @@ def test_order_create_positionids_addons(token_client, organizer, event, item, q
|
||||
"item": item.pk,
|
||||
"variation": None,
|
||||
"price": "23.00",
|
||||
"attendee_name": "Peter",
|
||||
"attendee_name_parts": {"full_name": "Peter"},
|
||||
"attendee_email": None,
|
||||
"addon_to": None,
|
||||
"answers": [],
|
||||
@@ -1657,7 +1710,7 @@ def test_order_create_positionids_addons(token_client, organizer, event, item, q
|
||||
"item": item.pk,
|
||||
"variation": None,
|
||||
"price": "23.00",
|
||||
"attendee_name": "Peter",
|
||||
"attendee_name_parts": {"full_name": "Peter"},
|
||||
"attendee_email": None,
|
||||
"addon_to": 1,
|
||||
"answers": [],
|
||||
@@ -1685,7 +1738,7 @@ def test_order_create_positionid_validation(token_client, organizer, event, item
|
||||
"item": item.pk,
|
||||
"variation": None,
|
||||
"price": "23.00",
|
||||
"attendee_name": "Peter",
|
||||
"attendee_name_parts": {"full_name": "Peter"},
|
||||
"attendee_email": None,
|
||||
"addon_to": None,
|
||||
"answers": [],
|
||||
@@ -1696,7 +1749,7 @@ def test_order_create_positionid_validation(token_client, organizer, event, item
|
||||
"item": item.pk,
|
||||
"variation": None,
|
||||
"price": "23.00",
|
||||
"attendee_name": "Peter",
|
||||
"attendee_name_parts": {"full_name": "Peter"},
|
||||
"attendee_email": None,
|
||||
"addon_to": 2,
|
||||
"answers": [],
|
||||
@@ -1727,7 +1780,7 @@ def test_order_create_positionid_validation(token_client, organizer, event, item
|
||||
"item": item.pk,
|
||||
"variation": None,
|
||||
"price": "23.00",
|
||||
"attendee_name": "Peter",
|
||||
"attendee_name_parts": {"full_name": "Peter"},
|
||||
"attendee_email": None,
|
||||
"addon_to": None,
|
||||
"answers": [],
|
||||
@@ -1737,7 +1790,7 @@ def test_order_create_positionid_validation(token_client, organizer, event, item
|
||||
"item": item.pk,
|
||||
"variation": None,
|
||||
"price": "23.00",
|
||||
"attendee_name": "Peter",
|
||||
"attendee_name_parts": {"full_name": "Peter"},
|
||||
"attendee_email": None,
|
||||
"addon_to": 2,
|
||||
"answers": [],
|
||||
@@ -1761,7 +1814,7 @@ def test_order_create_positionid_validation(token_client, organizer, event, item
|
||||
"item": item.pk,
|
||||
"variation": None,
|
||||
"price": "23.00",
|
||||
"attendee_name": "Peter",
|
||||
"attendee_name_parts": {"full_name": "Peter"},
|
||||
"attendee_email": None,
|
||||
"answers": [],
|
||||
"subevent": None
|
||||
@@ -1770,7 +1823,7 @@ def test_order_create_positionid_validation(token_client, organizer, event, item
|
||||
"item": item.pk,
|
||||
"variation": None,
|
||||
"price": "23.00",
|
||||
"attendee_name": "Peter",
|
||||
"attendee_name_parts": {"full_name": "Peter"},
|
||||
"attendee_email": None,
|
||||
"answers": [],
|
||||
"subevent": None
|
||||
@@ -1797,7 +1850,7 @@ def test_order_create_positionid_validation(token_client, organizer, event, item
|
||||
"item": item.pk,
|
||||
"variation": None,
|
||||
"price": "23.00",
|
||||
"attendee_name": "Peter",
|
||||
"attendee_name_parts": {"full_name": "Peter"},
|
||||
"attendee_email": None,
|
||||
"answers": [],
|
||||
"subevent": None
|
||||
@@ -1807,7 +1860,7 @@ def test_order_create_positionid_validation(token_client, organizer, event, item
|
||||
"item": item.pk,
|
||||
"variation": None,
|
||||
"price": "23.00",
|
||||
"attendee_name": "Peter",
|
||||
"attendee_name_parts": {"full_name": "Peter"},
|
||||
"attendee_email": None,
|
||||
"answers": [],
|
||||
"subevent": None
|
||||
@@ -2066,7 +2119,7 @@ def test_order_create_quota_validation(token_client, organizer, event, item, quo
|
||||
"item": item.pk,
|
||||
"variation": None,
|
||||
"price": "23.00",
|
||||
"attendee_name": "Peter",
|
||||
"attendee_name_parts": {"full_name": "Peter"},
|
||||
"attendee_email": None,
|
||||
"addon_to": None,
|
||||
"answers": [],
|
||||
@@ -2077,7 +2130,7 @@ def test_order_create_quota_validation(token_client, organizer, event, item, quo
|
||||
"item": item.pk,
|
||||
"variation": None,
|
||||
"price": "23.00",
|
||||
"attendee_name": "Peter",
|
||||
"attendee_name_parts": {"full_name": "Peter"},
|
||||
"attendee_email": None,
|
||||
"addon_to": 1,
|
||||
"answers": [],
|
||||
|
||||
@@ -121,7 +121,10 @@ def test_address_vat_id(env):
|
||||
event, order = env
|
||||
event.settings.set('invoice_language', 'en')
|
||||
InvoiceAddress.objects.create(company='Acme Company', street='221B Baker Street',
|
||||
name='Sherlock Holmes', zipcode='12345', city='London', country_old='UK',
|
||||
name_parts={'full_name': 'Sherlock Holmes', '_scheme': 'full'},
|
||||
zipcode='12345',
|
||||
city='London',
|
||||
country_old='UK',
|
||||
country='', vat_id='UK1234567', order=order)
|
||||
inv = generate_invoice(order)
|
||||
assert inv.invoice_to == "Acme Company\nSherlock Holmes\n221B Baker Street\n12345 London\nUK\nVAT-ID: UK1234567"
|
||||
|
||||
@@ -34,7 +34,7 @@ def order(event):
|
||||
default_price=Decimal('23.00'), admission=True)
|
||||
OrderPosition.objects.create(
|
||||
order=o, item=ticket, variation=None,
|
||||
price=Decimal("23.00"), attendee_name="Peter", positionid=1
|
||||
price=Decimal("23.00"), attendee_name_parts={'full_name': "Peter"}, positionid=1
|
||||
)
|
||||
return o
|
||||
|
||||
|
||||
@@ -309,7 +309,7 @@ class PaymentReminderTests(TestCase):
|
||||
default_price=Decimal('23.00'), admission=True)
|
||||
self.op1 = OrderPosition.objects.create(
|
||||
order=self.order, item=self.ticket, variation=None,
|
||||
price=Decimal("23.00"), attendee_name="Peter", positionid=1
|
||||
price=Decimal("23.00"), attendee_name_parts={'full_name': "Peter"}, positionid=1
|
||||
)
|
||||
djmail.outbox = []
|
||||
|
||||
@@ -357,7 +357,7 @@ class DownloadReminderTests(TestCase):
|
||||
default_price=Decimal('23.00'), admission=True)
|
||||
self.op1 = OrderPosition.objects.create(
|
||||
order=self.order, item=self.ticket, variation=None,
|
||||
price=Decimal("23.00"), attendee_name="Peter", positionid=1
|
||||
price=Decimal("23.00"), attendee_name_parts={"full_name": "Peter"}, positionid=1
|
||||
)
|
||||
djmail.outbox = []
|
||||
|
||||
@@ -411,11 +411,11 @@ class OrderChangeManagerTests(TestCase):
|
||||
default_price=Decimal('12.00'))
|
||||
self.op1 = OrderPosition.objects.create(
|
||||
order=self.order, item=self.ticket, variation=None,
|
||||
price=Decimal("23.00"), attendee_name="Peter", positionid=1
|
||||
price=Decimal("23.00"), attendee_name_parts={'full_name': "Peter"}, positionid=1
|
||||
)
|
||||
self.op2 = OrderPosition.objects.create(
|
||||
order=self.order, item=self.ticket, variation=None,
|
||||
price=Decimal("23.00"), attendee_name="Dieter", positionid=2
|
||||
price=Decimal("23.00"), attendee_name_parts={'full_name': "Dieter"}, positionid=2
|
||||
)
|
||||
self.ocm = OrderChangeManager(self.order, None)
|
||||
self.quota = self.event.quotas.create(name='Test', size=None)
|
||||
|
||||
@@ -154,11 +154,11 @@ def test_availability_date_order_relative_subevents(event):
|
||||
)
|
||||
OrderPosition.objects.create(
|
||||
order=order, item=ticket, variation=None, subevent=se1,
|
||||
price=Decimal("23.00"), attendee_name="Peter", positionid=1
|
||||
price=Decimal("23.00"), attendee_name_parts={'full_name': "Peter"}, positionid=1
|
||||
)
|
||||
OrderPosition.objects.create(
|
||||
order=order, item=ticket, variation=None, subevent=se2,
|
||||
price=Decimal("23.00"), attendee_name="Dieter", positionid=2
|
||||
price=Decimal("23.00"), attendee_name_parts={'full_name': "Dieter"}, positionid=2
|
||||
)
|
||||
|
||||
prov = DummyPaymentProvider(event)
|
||||
|
||||
@@ -54,7 +54,7 @@ def order(event, item):
|
||||
item=item,
|
||||
variation=None,
|
||||
price=Decimal("14"),
|
||||
attendee_name="Peter",
|
||||
attendee_name_parts={'full_name': "Peter", "_scheme": "full"},
|
||||
attendee_email="foo@example.org"
|
||||
)
|
||||
return o
|
||||
@@ -155,7 +155,7 @@ def test_attendee_name_shredder(event, order):
|
||||
}
|
||||
s.shred_data()
|
||||
order.refresh_from_db()
|
||||
assert order.positions.first().attendee_name is None
|
||||
assert not order.positions.first().attendee_name
|
||||
l1.refresh_from_db()
|
||||
assert 'Hans' not in l1.data
|
||||
assert 'Foo' in l1.data
|
||||
@@ -186,6 +186,7 @@ def test_invoice_address_shredder(event, order):
|
||||
'is_business': False,
|
||||
'last_modified': ia.last_modified.isoformat().replace('+00:00', 'Z'),
|
||||
'name': '',
|
||||
'name_parts': {},
|
||||
'street': '221B Baker Street',
|
||||
'vat_id': '',
|
||||
'vat_id_validated': False,
|
||||
|
||||
@@ -45,7 +45,7 @@ def dashboard_env():
|
||||
item=item_ticket,
|
||||
variation=None,
|
||||
price=Decimal("23"),
|
||||
attendee_name="Peter"
|
||||
attendee_name_parts={"full_name": "Peter"}
|
||||
)
|
||||
OrderPosition.objects.create(
|
||||
order=order_paid,
|
||||
@@ -77,7 +77,7 @@ def test_dashboard_pending_not_count(dashboard_env):
|
||||
item=dashboard_env[4],
|
||||
variation=None,
|
||||
price=Decimal("23"),
|
||||
attendee_name="NotPaid"
|
||||
attendee_name_parts={'full_name': "NotPaid"}
|
||||
)
|
||||
assert '0/2' in c[0]['content']
|
||||
|
||||
@@ -149,14 +149,14 @@ def checkin_list_env():
|
||||
item=item_ticket,
|
||||
variation=None,
|
||||
price=Decimal("23"),
|
||||
attendee_name="Pending"
|
||||
attendee_name_parts={'full_name': "Pending"}
|
||||
)
|
||||
op_a1_ticket = OrderPosition.objects.create(
|
||||
order=order_a1,
|
||||
item=item_ticket,
|
||||
variation=None,
|
||||
price=Decimal("23"),
|
||||
attendee_name="A1"
|
||||
attendee_name_parts={'full_name': "A1"}
|
||||
)
|
||||
op_a1_mascot = OrderPosition.objects.create(
|
||||
order=order_a1,
|
||||
@@ -169,14 +169,14 @@ def checkin_list_env():
|
||||
item=item_ticket,
|
||||
variation=None,
|
||||
price=Decimal("23"),
|
||||
attendee_name="A2"
|
||||
attendee_name_parts={'full_name': "A2"}
|
||||
)
|
||||
op_a3_ticket = OrderPosition.objects.create(
|
||||
order=order_a3,
|
||||
item=item_ticket,
|
||||
variation=None,
|
||||
price=Decimal("23"),
|
||||
attendee_name="a4", # a3 attendee is a4
|
||||
attendee_name_parts={'full_name': "a4"}, # a3 attendee is a4
|
||||
attendee_email="a3company@dummy.test"
|
||||
)
|
||||
|
||||
@@ -339,14 +339,14 @@ def checkin_list_with_addon_env():
|
||||
item=item_ticket,
|
||||
variation=None,
|
||||
price=Decimal("23"),
|
||||
attendee_name="Pending"
|
||||
attendee_name_parts={'full_name': "Pending"}
|
||||
)
|
||||
op_a1_ticket = OrderPosition.objects.create(
|
||||
order=order_a1,
|
||||
item=item_ticket,
|
||||
variation=None,
|
||||
price=Decimal("23"),
|
||||
attendee_name="A1"
|
||||
attendee_name_parts={'full_name': "A1"}
|
||||
)
|
||||
op_a1_workshop = OrderPosition.objects.create(
|
||||
order=order_a1,
|
||||
@@ -360,7 +360,7 @@ def checkin_list_with_addon_env():
|
||||
item=item_ticket,
|
||||
variation=None,
|
||||
price=Decimal("23"),
|
||||
attendee_name="A2"
|
||||
attendee_name_parts={'full_name': "A2"}
|
||||
)
|
||||
|
||||
# checkin
|
||||
|
||||
@@ -187,13 +187,13 @@ class QuestionsTest(ItemFormTest):
|
||||
expires=now() + datetime.timedelta(days=10),
|
||||
total=14, locale='en')
|
||||
op = OrderPosition.objects.create(order=o, item=item1, variation=None, price=Decimal("14"),
|
||||
attendee_name="Peter")
|
||||
attendee_name_parts={'full_name': "Peter"})
|
||||
op.answers.create(question=c, answer='42')
|
||||
op = OrderPosition.objects.create(order=o, item=item1, variation=None, price=Decimal("14"),
|
||||
attendee_name="Michael")
|
||||
attendee_name_parts={'full_name': "Michael"})
|
||||
op.answers.create(question=c, answer='42')
|
||||
op = OrderPosition.objects.create(order=o, item=item1, variation=None, price=Decimal("14"),
|
||||
attendee_name="Petra")
|
||||
attendee_name_parts={'full_name': "Petra"})
|
||||
op.answers.create(question=c, answer='39')
|
||||
|
||||
doc = self.get_doc('/control/event/%s/%s/questions/%s/' % (self.orga1.slug, self.event1.slug, c.id))
|
||||
@@ -414,7 +414,7 @@ class ItemsTest(ItemFormTest):
|
||||
item=self.item1,
|
||||
variation=None,
|
||||
price=Decimal("14"),
|
||||
attendee_name="Peter"
|
||||
attendee_name_parts={'full_name': "Peter"}
|
||||
)
|
||||
self.client.post('/control/event/%s/%s/items/%d/delete' % (self.orga1.slug, self.event1.slug, self.item1.id),
|
||||
{})
|
||||
|
||||
@@ -51,7 +51,7 @@ def env():
|
||||
item=ticket,
|
||||
variation=None,
|
||||
price=Decimal("14"),
|
||||
attendee_name="Peter"
|
||||
attendee_name_parts={'full_name': "Peter", "_scheme": "full"}
|
||||
)
|
||||
return event, user, o, ticket
|
||||
|
||||
@@ -333,7 +333,7 @@ def test_order_invoice_create_ok(client, env):
|
||||
def test_order_invoice_regenerate(client, env):
|
||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
i = generate_invoice(env[2])
|
||||
InvoiceAddress.objects.create(name='Foo', order=env[2])
|
||||
InvoiceAddress.objects.create(name_parts={'full_name': 'Foo', "_scheme": "full"}, order=env[2])
|
||||
env[0].settings.set('invoice_generate', 'admin')
|
||||
response = client.post('/control/event/dummy/dummy/orders/FOO/invoices/%d/regenerate' % i.pk, {}, follow=True)
|
||||
assert 'alert-success' in response.rendered_content
|
||||
@@ -362,7 +362,7 @@ def test_order_invoice_regenerate_unknown(client, env):
|
||||
def test_order_invoice_reissue(client, env):
|
||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
i = generate_invoice(env[2])
|
||||
InvoiceAddress.objects.create(name='Foo', order=env[2])
|
||||
InvoiceAddress.objects.create(name_parts={'full_name': 'Foo', "_scheme": "full"}, order=env[2])
|
||||
env[0].settings.set('invoice_generate', 'admin')
|
||||
response = client.post('/control/event/dummy/dummy/orders/FOO/invoices/%d/reissue' % i.pk, {}, follow=True)
|
||||
assert 'alert-success' in response.rendered_content
|
||||
@@ -528,7 +528,7 @@ def test_order_extend_expired_quota_partial(client, env):
|
||||
item=env[3],
|
||||
variation=None,
|
||||
price=Decimal("14"),
|
||||
attendee_name="Peter"
|
||||
attendee_name_parts={'full_name': "Peter", "_scheme": "full"}
|
||||
)
|
||||
o.expires = now() - timedelta(days=5)
|
||||
o.status = Order.STATUS_EXPIRED
|
||||
@@ -745,11 +745,11 @@ class OrderChangeTests(SoupTest):
|
||||
default_price=Decimal('12.00'))
|
||||
self.op1 = OrderPosition.objects.create(
|
||||
order=self.order, item=self.ticket, variation=None,
|
||||
price=Decimal("23.00"), attendee_name="Peter"
|
||||
price=Decimal("23.00"), attendee_name_parts={'full_name': "Peter", "_scheme": "full"}
|
||||
)
|
||||
self.op2 = OrderPosition.objects.create(
|
||||
order=self.order, item=self.ticket, variation=None,
|
||||
price=Decimal("23.00"), attendee_name="Dieter"
|
||||
price=Decimal("23.00"), attendee_name_parts={'full_name': "Dieter", "_scheme": "full"}
|
||||
)
|
||||
self.quota = self.event.quotas.create(name="All", size=100)
|
||||
self.quota.items.add(self.ticket)
|
||||
|
||||
@@ -30,7 +30,7 @@ class OrderSearchTest(SoupTest):
|
||||
datetime=now(), expires=now() + datetime.timedelta(days=10),
|
||||
total=14, locale='en'
|
||||
)
|
||||
InvoiceAddress.objects.create(order=o1, company="Test Ltd.", name="Peter Miller")
|
||||
InvoiceAddress.objects.create(order=o1, company="Test Ltd.", name_parts={'full_name': "Peter Miller", "_scheme": "full"})
|
||||
ticket1 = Item.objects.create(event=self.event1, name='Early-bird ticket',
|
||||
category=None, default_price=23,
|
||||
admission=True)
|
||||
@@ -39,7 +39,7 @@ class OrderSearchTest(SoupTest):
|
||||
item=ticket1,
|
||||
variation=None,
|
||||
price=Decimal("14"),
|
||||
attendee_name="Peter",
|
||||
attendee_name_parts={'full_name': "Peter", "_scheme": "full"},
|
||||
attendee_email="att@att.com"
|
||||
)
|
||||
|
||||
@@ -57,7 +57,7 @@ class OrderSearchTest(SoupTest):
|
||||
item=ticket2,
|
||||
variation=None,
|
||||
price=Decimal("14"),
|
||||
attendee_name="Mark"
|
||||
attendee_name_parts={'full_name': "Mark", "_scheme": "full"}
|
||||
)
|
||||
|
||||
self.team = Team.objects.create(organizer=self.orga1, can_view_orders=True)
|
||||
|
||||
@@ -29,11 +29,11 @@ def env():
|
||||
shirt_red = ItemVariation.objects.create(item=shirt, default_price=14, value="Red")
|
||||
OrderPosition.objects.create(
|
||||
order=o1, item=shirt, variation=shirt_red,
|
||||
price=12, attendee_name=None, secret='1234'
|
||||
price=12, attendee_name_parts={}, secret='1234'
|
||||
)
|
||||
OrderPosition.objects.create(
|
||||
order=o1, item=shirt, variation=shirt_red,
|
||||
price=12, attendee_name=None, secret='5678'
|
||||
price=12, attendee_name_parts={}, secret='5678'
|
||||
)
|
||||
return event, o1, shirt
|
||||
|
||||
|
||||
@@ -36,11 +36,11 @@ def env():
|
||||
)
|
||||
op1 = OrderPosition.objects.create(
|
||||
order=o1, item=shirt, variation=shirt_red,
|
||||
price=12, attendee_name=None, secret='1234'
|
||||
price=12, attendee_name_parts={}, secret='1234'
|
||||
)
|
||||
op2 = OrderPosition.objects.create(
|
||||
order=o1, item=ticket,
|
||||
price=23, attendee_name="Peter", secret='5678910'
|
||||
price=23, attendee_name_parts={"full_name": "Peter", "_scheme": "full"}, secret='5678910'
|
||||
)
|
||||
cl1 = event.checkin_lists.create(name="Foo", all_products=True)
|
||||
cl2 = event.checkin_lists.create(name="Bar", all_products=True)
|
||||
@@ -273,7 +273,7 @@ def test_search_restricted(client, env):
|
||||
@pytest.mark.django_db
|
||||
def test_search_invoice_name(client, env):
|
||||
AppConfiguration.objects.create(event=env[0], key='abcdefg', list=env[5])
|
||||
InvoiceAddress.objects.create(order=env[2], name="John")
|
||||
InvoiceAddress.objects.create(order=env[2], name_parts={"full_name": "John", "_scheme": "full"})
|
||||
resp = client.get('/pretixdroid/api/%s/%s/search/?key=%s&query=%s' % (
|
||||
env[0].organizer.slug, env[0].slug, 'abcdefg', 'John'))
|
||||
jdata = json.loads(resp.content.decode("utf-8"))
|
||||
|
||||
@@ -39,11 +39,11 @@ def env():
|
||||
)
|
||||
op1 = OrderPosition.objects.create(
|
||||
order=o1, item=shirt, variation=shirt_red,
|
||||
price=12, attendee_name=None, secret='1234', subevent=se1
|
||||
price=12, attendee_name_parts={}, secret='1234', subevent=se1
|
||||
)
|
||||
op2 = OrderPosition.objects.create(
|
||||
order=o1, item=ticket,
|
||||
price=23, attendee_name="Peter", secret='5678910', subevent=se2
|
||||
price=23, attendee_name_parts={'full_name': "Peter"}, secret='5678910', subevent=se2
|
||||
)
|
||||
cl1 = event.checkin_lists.create(name="Foo", all_products=True, subevent=se1)
|
||||
cl2 = event.checkin_lists.create(name="Foo", all_products=True, subevent=se2)
|
||||
|
||||
108
src/tests/plugins/test_checkinlist.py
Normal file
108
src/tests/plugins/test_checkinlist.py
Normal 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"
|
||||
""")
|
||||
@@ -29,11 +29,11 @@ def env0():
|
||||
shirt_red = ItemVariation.objects.create(item=shirt, default_price=14, value="Red")
|
||||
OrderPosition.objects.create(
|
||||
order=o1, item=shirt, variation=shirt_red,
|
||||
price=12, attendee_name=None, secret='1234'
|
||||
price=12, attendee_name_parts={}, secret='1234'
|
||||
)
|
||||
OrderPosition.objects.create(
|
||||
order=o1, item=shirt, variation=shirt_red,
|
||||
price=12, attendee_name=None, secret='5678'
|
||||
price=12, attendee_name_parts={}, secret='5678'
|
||||
)
|
||||
return event, o1
|
||||
|
||||
|
||||
@@ -539,11 +539,11 @@ class CheckoutTestCase(TestCase):
|
||||
)
|
||||
response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True)
|
||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||
self.assertEqual(len(doc.select('input[name=%s-attendee_name]' % cr1.id)), 1)
|
||||
self.assertEqual(len(doc.select('input[name=%s-attendee_name_parts_0]' % cr1.id)), 1)
|
||||
|
||||
# Not all required fields filled out, expect failure
|
||||
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
|
||||
'%s-attendee_name' % cr1.id: '',
|
||||
'%s-attendee_name_parts_0' % cr1.id: '',
|
||||
'email': 'admin@localhost'
|
||||
}, follow=True)
|
||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||
@@ -551,7 +551,7 @@ class CheckoutTestCase(TestCase):
|
||||
|
||||
# Corrected request
|
||||
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
|
||||
'%s-attendee_name' % cr1.id: 'Peter',
|
||||
'%s-attendee_name_parts_0' % cr1.id: 'Peter',
|
||||
'email': 'admin@localhost'
|
||||
}, follow=True)
|
||||
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
|
||||
@@ -560,6 +560,42 @@ class CheckoutTestCase(TestCase):
|
||||
cr1 = CartPosition.objects.get(id=cr1.id)
|
||||
self.assertEqual(cr1.attendee_name, 'Peter')
|
||||
|
||||
def test_attendee_name_scheme(self):
|
||||
self.event.settings.set('attendee_names_asked', True)
|
||||
self.event.settings.set('attendee_names_required', True)
|
||||
self.event.settings.set('name_scheme', 'title_given_middle_family')
|
||||
cr1 = CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||
price=23, expires=now() + timedelta(minutes=10)
|
||||
)
|
||||
response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True)
|
||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||
self.assertEqual(len(doc.select('input[name=%s-attendee_name_parts_0]' % cr1.id)), 1)
|
||||
self.assertEqual(len(doc.select('input[name=%s-attendee_name_parts_1]' % cr1.id)), 1)
|
||||
self.assertEqual(len(doc.select('input[name=%s-attendee_name_parts_2]' % cr1.id)), 1)
|
||||
self.assertEqual(len(doc.select('input[name=%s-attendee_name_parts_3]' % cr1.id)), 1)
|
||||
|
||||
# Not all required fields filled out, expect failure
|
||||
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
|
||||
'%s-attendee_name_parts_0' % cr1.id: 'Mr',
|
||||
'%s-attendee_name_parts_1' % cr1.id: 'John',
|
||||
'%s-attendee_name_parts_2' % cr1.id: 'F',
|
||||
'%s-attendee_name_parts_3' % cr1.id: 'Kennedy',
|
||||
'email': 'admin@localhost'
|
||||
})
|
||||
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
|
||||
target_status_code=200)
|
||||
|
||||
cr1 = CartPosition.objects.get(id=cr1.id)
|
||||
self.assertEqual(cr1.attendee_name, 'Mr John F Kennedy')
|
||||
self.assertEqual(cr1.attendee_name_parts, {
|
||||
'given_name': 'John',
|
||||
'title': 'Mr',
|
||||
'middle_name': 'F',
|
||||
'family_name': 'Kennedy',
|
||||
"_scheme": "title_given_middle_family"
|
||||
})
|
||||
|
||||
def test_attendee_name_optional(self):
|
||||
self.event.settings.set('attendee_names_asked', True)
|
||||
self.event.settings.set('attendee_names_required', False)
|
||||
@@ -569,22 +605,23 @@ class CheckoutTestCase(TestCase):
|
||||
)
|
||||
response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True)
|
||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||
self.assertEqual(len(doc.select('input[name=%s-attendee_name]' % cr1.id)), 1)
|
||||
self.assertEqual(len(doc.select('input[name=%s-attendee_name_parts_0]' % cr1.id)), 1)
|
||||
|
||||
# Not all fields filled out, expect success
|
||||
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
|
||||
'%s-attendee_name' % cr1.id: '',
|
||||
'%s-attendee_name_parts_0' % cr1.id: '',
|
||||
'email': 'admin@localhost'
|
||||
}, follow=True)
|
||||
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
|
||||
target_status_code=200)
|
||||
|
||||
cr1 = CartPosition.objects.get(id=cr1.id)
|
||||
self.assertIsNone(cr1.attendee_name)
|
||||
assert not cr1.attendee_name
|
||||
|
||||
def test_invoice_address_required(self):
|
||||
self.event.settings.invoice_address_asked = True
|
||||
self.event.settings.invoice_address_required = True
|
||||
self.event.settings.set('name_scheme', 'title_given_middle_family')
|
||||
|
||||
CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||
@@ -609,7 +646,10 @@ class CheckoutTestCase(TestCase):
|
||||
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
|
||||
'is_business': 'business',
|
||||
'company': 'Foo',
|
||||
'name': 'Bar',
|
||||
'name_parts_0': 'Mr',
|
||||
'name_parts_1': 'John',
|
||||
'name_parts_2': '',
|
||||
'name_parts_3': 'Kennedy',
|
||||
'street': 'Baz',
|
||||
'zipcode': '12345',
|
||||
'city': 'Here',
|
||||
@@ -619,6 +659,15 @@ class CheckoutTestCase(TestCase):
|
||||
}, follow=True)
|
||||
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
|
||||
target_status_code=200)
|
||||
ia = InvoiceAddress.objects.last()
|
||||
assert ia.name_parts == {
|
||||
'title': 'Mr',
|
||||
'given_name': 'John',
|
||||
'middle_name': '',
|
||||
'family_name': 'Kennedy',
|
||||
"_scheme": "title_given_middle_family"
|
||||
}
|
||||
assert ia.name_cached == 'Mr John Kennedy'
|
||||
|
||||
def test_invoice_address_optional(self):
|
||||
self.event.settings.invoice_address_asked = True
|
||||
@@ -653,7 +702,7 @@ class CheckoutTestCase(TestCase):
|
||||
)
|
||||
response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True)
|
||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||
self.assertEqual(len(doc.select('input[name=name]')), 1)
|
||||
self.assertEqual(len(doc.select('input[name=name_parts_0]')), 1)
|
||||
self.assertEqual(len(doc.select('input[name=street]')), 0)
|
||||
|
||||
# Not all required fields filled out, expect failure
|
||||
@@ -665,7 +714,7 @@ class CheckoutTestCase(TestCase):
|
||||
|
||||
# Corrected request
|
||||
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
|
||||
'name': 'Raphael',
|
||||
'name_parts_0': 'Raphael',
|
||||
'email': 'admin@localhost'
|
||||
}, follow=True)
|
||||
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
|
||||
@@ -762,7 +811,7 @@ class CheckoutTestCase(TestCase):
|
||||
self.event.settings.set('invoice_address_required', True)
|
||||
ia = InvoiceAddress.objects.create(
|
||||
is_business=True, vat_id='ATU1234567', vat_id_validated=True,
|
||||
country=Country('DE'), name='Foo', street='Foo'
|
||||
country=Country('DE'), name_parts={'full_name': 'Foo', "_scheme": "full"}, name_cached='Foo', street='Foo'
|
||||
)
|
||||
self._set_session('invoice_address', ia.pk)
|
||||
CartPosition.objects.create(
|
||||
@@ -786,7 +835,7 @@ class CheckoutTestCase(TestCase):
|
||||
self.event.settings.set('invoice_address_required', True)
|
||||
ia = InvoiceAddress.objects.create(
|
||||
is_business=True, vat_id='ATU1234567', vat_id_validated=True,
|
||||
country=Country('CH'), name='Foo', street='Foo'
|
||||
country=Country('CH'), name_parts={'full_name': 'Foo', "_scheme": "full"}, name_cached='Foo', street='Foo'
|
||||
)
|
||||
self._set_session('invoice_address', ia.pk)
|
||||
CartPosition.objects.create(
|
||||
@@ -828,7 +877,7 @@ class CheckoutTestCase(TestCase):
|
||||
self.assertRedirects(response, '/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug),
|
||||
target_status_code=200)
|
||||
|
||||
cr1.attendee_name = 'Peter'
|
||||
cr1.attendee_name_parts = {"full_name": 'Peter', "_scheme": "full"}
|
||||
cr1.save()
|
||||
q1 = Question.objects.create(
|
||||
event=self.event, question='Age', type=Question.TYPE_NUMBER,
|
||||
|
||||
@@ -61,7 +61,7 @@ class OrdersTest(TestCase):
|
||||
item=self.ticket,
|
||||
variation=None,
|
||||
price=Decimal("23"),
|
||||
attendee_name="Peter"
|
||||
attendee_name_parts={'full_name': "Peter"}
|
||||
)
|
||||
self.not_my_order = Order.objects.create(
|
||||
status=Order.STATUS_PENDING,
|
||||
@@ -147,12 +147,12 @@ class OrdersTest(TestCase):
|
||||
response = self.client.get(
|
||||
'/%s/%s/order/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret))
|
||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||
self.assertEqual(len(doc.select('input[name=%s-attendee_name]' % self.ticket_pos.id)), 1)
|
||||
self.assertEqual(len(doc.select('input[name=%s-attendee_name_parts_0]' % self.ticket_pos.id)), 1)
|
||||
|
||||
# Not all fields filled out, expect success
|
||||
response = self.client.post(
|
||||
'/%s/%s/order/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), {
|
||||
'%s-attendee_name' % self.ticket_pos.id: '',
|
||||
'%s-attendee_name_parts_0' % self.ticket_pos.id: '',
|
||||
}, follow=True)
|
||||
self.assertRedirects(response,
|
||||
'/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code,
|
||||
@@ -168,19 +168,19 @@ class OrdersTest(TestCase):
|
||||
response = self.client.get(
|
||||
'/%s/%s/order/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret))
|
||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||
self.assertEqual(len(doc.select('input[name=%s-attendee_name]' % self.ticket_pos.id)), 1)
|
||||
self.assertEqual(len(doc.select('input[name=%s-attendee_name_parts_0]' % self.ticket_pos.id)), 1)
|
||||
|
||||
# Not all required fields filled out, expect failure
|
||||
response = self.client.post(
|
||||
'/%s/%s/order/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), {
|
||||
'%s-attendee_name' % self.ticket_pos.id: '',
|
||||
'%s-attendee_name_parts_0' % self.ticket_pos.id: '',
|
||||
}, follow=True)
|
||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||
self.assertGreaterEqual(len(doc.select('.has-error')), 1)
|
||||
|
||||
response = self.client.post(
|
||||
'/%s/%s/order/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), {
|
||||
'%s-attendee_name' % self.ticket_pos.id: 'Peter',
|
||||
'%s-attendee_name_parts_0' % self.ticket_pos.id: 'Peter',
|
||||
}, follow=True)
|
||||
self.assertRedirects(response, '/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code,
|
||||
self.order.secret),
|
||||
|
||||
@@ -30,7 +30,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
||||
item=self.ticket,
|
||||
variation=None,
|
||||
price=Decimal("23"),
|
||||
attendee_name="Peter"
|
||||
attendee_name_parts={'full_name': "Peter"}
|
||||
)
|
||||
|
||||
def test_iframe_entry_view_wrapper(self):
|
||||
|
||||
Reference in New Issue
Block a user