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

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

View File

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

View File

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

View File

@@ -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 && \

View File

@@ -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;

View File

@@ -21,6 +21,9 @@ To use pretix, you will need the following things:
.. warning:: Do not ever use SQLite in production. It will break.
.. warning:: 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.

View File

@@ -23,7 +23,7 @@ installation guides):
* A SMTP server to send out mails, e.g. `Postfix`_ on your machine or some third-party server you have credentials for
* A 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;

View File

@@ -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": [
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(

View File

@@ -12,6 +12,7 @@ from django.utils.translation import ugettext as _, ugettext_lazy
from pretix.base.models import InvoiceAddress, Order, OrderPosition
from pretix.base.models.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 '',

View File

@@ -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'):

View File

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

View File

@@ -0,0 +1,62 @@
# Generated by Django 2.1 on 2018-10-17 00:24
import jsonfallback.fields
from django.db import migrations
def set_attendee_name_parts(apps, schema_editor):
OrderPosition = apps.get_model('pretixbase', 'OrderPosition') # noqa
for op in OrderPosition.objects.exclude(attendee_name_cached=None).exclude(
attendee_name_cached__isnull=True).iterator():
op.attendee_name_parts = {'_legacy': op.attendee_name_cached}
CartPosition = apps.get_model('pretixbase', 'CartPosition') # noqa
for op in CartPosition.objects.exclude(attendee_name_cached=None).exclude(
attendee_name_cached__isnull=True).iterator():
op.attendee_name_parts = {'_legacy': op.attendee_name_cached}
InvoiceAddress = apps.get_model('pretixbase', 'InvoiceAddress') # noqa
for ia in InvoiceAddress.objects.exclude(name_cached=None).exclude(
name_cached__isnull=True).iterator():
op.name_parts = {'_legacy': ia.name_cached}
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0101_auto_20181025_2255'),
]
operations = [
migrations.RenameField(
model_name='cartposition',
old_name='attendee_name',
new_name='attendee_name_cached',
),
migrations.RenameField(
model_name='orderposition',
old_name='attendee_name',
new_name='attendee_name_cached',
),
migrations.RenameField(
model_name='invoiceaddress',
old_name='name',
new_name='name_cached',
),
migrations.AddField(
model_name='cartposition',
name='attendee_name_parts',
field=jsonfallback.fields.FallbackJSONField(null=False, default=dict),
preserve_default=False,
),
migrations.AddField(
model_name='orderposition',
name='attendee_name_parts',
field=jsonfallback.fields.FallbackJSONField(null=False, default=dict),
preserve_default=False,
),
migrations.AddField(
model_name='invoiceaddress',
name='name_parts',
field=jsonfallback.fields.FallbackJSONField(default=dict),
preserve_default=False,
),
migrations.RunPython(set_attendee_name_parts, migrations.RunPython.noop)
]

View File

@@ -75,7 +75,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
REQUIRED_FIELDS = []
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,

View File

@@ -26,10 +26,12 @@ from django.utils.timezone import make_aware, now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django_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)

View File

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

View File

@@ -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:

View File

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

View File

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

View File

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

View File

@@ -20,6 +20,7 @@ from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
from pretix.base.models import Event, Organizer, TaxRule
from pretix.base.models.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):

View File

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

View File

@@ -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" %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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" %}">

View File

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

View File

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

View File

@@ -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 {}
}
}

View File

@@ -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;
}
}
}

View File

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

View File

@@ -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;
}
}
}

View File

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

View File

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

View File

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

View File

@@ -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.*']),

View File

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

View File

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

View File

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

View File

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

View File

@@ -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": [],

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,

View File

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

View File

@@ -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),
{})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,108 @@
import datetime
from decimal import Decimal
import pytest
from django.utils.timezone import now
from pretix.base.models import Event, Item, Order, OrderPosition, Organizer
from pretix.plugins.checkinlists.exporters import CSVCheckinList
@pytest.fixture
def event():
"""Returns an event instance"""
o = Organizer.objects.create(name='Dummy', slug='dummy')
event = Event.objects.create(
organizer=o, name='Dummy', slug='dummy',
date_from=now(),
plugins='pretix.plugins.checkinlists,tests.testdummy',
)
event.settings.set('attendee_names_asked', True)
event.settings.set('name_scheme', 'title_given_middle_family')
event.settings.set('locales', ['en', 'de'])
event.checkin_lists.create(name="Default", all_products=True)
order_paid = Order.objects.create(
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PAID,
datetime=now(), expires=now() + datetime.timedelta(days=10),
total=33, locale='en'
)
item_ticket = Item.objects.create(event=event, name="Ticket", default_price=23, admission=True)
OrderPosition.objects.create(
order=order_paid,
item=item_ticket,
variation=None,
price=Decimal("23"),
attendee_name_parts={"title": "Mr", "given_name": "Peter", "middle_name": "A", "family_name": "Jones"},
secret='hutjztuxhkbtwnesv2suqv26k6ttytxx'
)
OrderPosition.objects.create(
order=order_paid,
item=item_ticket,
variation=None,
price=Decimal("13"),
attendee_name_parts={"title": "Mrs", "given_name": "Andrea", "middle_name": "J", "family_name": "Zulu"},
secret='ggsngqtnmhx74jswjngw3fk8pfwz2a7k'
)
return event
def clean(d):
return d.replace("\r", "").replace("\n", "")
@pytest.mark.django_db
def test_csv_simple(event):
c = CSVCheckinList(event)
_, _, content = c.render({
'list': event.checkin_lists.first().pk,
'secrets': True,
'sort': 'name',
'questions': []
})
assert clean(content.decode()) == clean(""""Order code","Attendee name","Attendee name: Title","Attendee name:
First name","Attendee name: Middle name","Attendee name: Family name","Product","Price","Checked in","Secret",
"E-mail"
"FOO","Mr Peter A Jones","Mr","Peter","A","Jones","Ticket","23.00","","hutjztuxhkbtwnesv2suqv26k6ttytxx",
"dummy@dummy.test"
"FOO","Mrs Andrea J Zulu","Mrs","Andrea","J","Zulu","Ticket","13.00","","ggsngqtnmhx74jswjngw3fk8pfwz2a7k",
"dummy@dummy.test"
""")
@pytest.mark.django_db
def test_csv_order_by_name_parts(event): # noqa
from django.conf import settings
if not settings.JSON_FIELD_AVAILABLE:
raise pytest.skip("Not supported on this database")
c = CSVCheckinList(event)
_, _, content = c.render({
'list': event.checkin_lists.first().pk,
'secrets': True,
'sort': 'name:given_name',
'questions': []
})
assert clean(content.decode()) == clean(""""Order code","Attendee name","Attendee name: Title",
"Attendee name: First name","Attendee name: Middle name","Attendee name: Family name","Product","Price",
"Checked in","Secret","E-mail"
"FOO","Mrs Andrea J Zulu","Mrs","Andrea","J","Zulu","Ticket","13.00","","ggsngqtnmhx74jswjngw3fk8pfwz2a7k",
"dummy@dummy.test"
"FOO","Mr Peter A Jones","Mr","Peter","A","Jones","Ticket","23.00","","hutjztuxhkbtwnesv2suqv26k6ttytxx",
"dummy@dummy.test"
""")
c = CSVCheckinList(event)
_, _, content = c.render({
'list': event.checkin_lists.first().pk,
'secrets': True,
'sort': 'name:family_name',
'questions': []
})
assert clean(content.decode()) == clean(""""Order code","Attendee name","Attendee name: Title",
"Attendee name: First name","Attendee name: Middle name","Attendee name: Family name","Product","Price",
"Checked in","Secret","E-mail"
"FOO","Mr Peter A Jones","Mr","Peter","A","Jones","Ticket","23.00","","hutjztuxhkbtwnesv2suqv26k6ttytxx",
"dummy@dummy.test"
"FOO","Mrs Andrea J Zulu","Mrs","Andrea","J","Zulu","Ticket","13.00","","ggsngqtnmhx74jswjngw3fk8pfwz2a7k",
"dummy@dummy.test"
""")

View File

@@ -29,11 +29,11 @@ def env0():
shirt_red = ItemVariation.objects.create(item=shirt, default_price=14, value="Red")
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

View File

@@ -539,11 +539,11 @@ class CheckoutTestCase(TestCase):
)
response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True)
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,

View File

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

View File

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