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