Compare commits

..

1 Commits

Author SHA1 Message Date
Raphael Michel
65f8b68634 Fix packaging bugs 2020-10-26 10:35:33 +01:00
249 changed files with 42168 additions and 78236 deletions

View File

@@ -1,4 +1,4 @@
FROM python:3.8 FROM python:3.6
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y --no-install-recommends \ apt-get install -y --no-install-recommends \
@@ -30,8 +30,7 @@ RUN apt-get update && \
mkdir /data && \ mkdir /data && \
useradd -ms /bin/bash -d /pretix -u 15371 pretixuser && \ useradd -ms /bin/bash -d /pretix -u 15371 pretixuser && \
echo 'pretixuser ALL=(ALL) NOPASSWD:SETENV: /usr/bin/supervisord' >> /etc/sudoers && \ echo 'pretixuser ALL=(ALL) NOPASSWD:SETENV: /usr/bin/supervisord' >> /etc/sudoers && \
mkdir /static && \ mkdir /static
mkdir /etc/supervisord
ENV LC_ALL=C.UTF-8 \ ENV LC_ALL=C.UTF-8 \
DJANGO_SETTINGS_MODULE=production_settings DJANGO_SETTINGS_MODULE=production_settings
@@ -48,13 +47,12 @@ RUN pip3 install -U \
-r requirements.txt \ -r requirements.txt \
-r requirements/memcached.txt \ -r requirements/memcached.txt \
-r requirements/mysql.txt \ -r requirements/mysql.txt \
gunicorn django-extensions ipython && \ -r requirements/redis.txt \
gunicorn && \
rm -rf ~/.cache/pip rm -rf ~/.cache/pip
COPY deployment/docker/pretix.bash /usr/local/bin/pretix COPY deployment/docker/pretix.bash /usr/local/bin/pretix
COPY deployment/docker/supervisord /etc/supervisord COPY deployment/docker/supervisord.conf /etc/supervisord.conf
COPY deployment/docker/supervisord.all.conf /etc/supervisord.all.conf
COPY deployment/docker/supervisord.web.conf /etc/supervisord.web.conf
COPY deployment/docker/nginx.conf /etc/nginx/nginx.conf COPY deployment/docker/nginx.conf /etc/nginx/nginx.conf
COPY deployment/docker/production_settings.py /pretix/src/production_settings.py COPY deployment/docker/production_settings.py /pretix/src/production_settings.py
COPY src /pretix/src COPY src /pretix/src

View File

@@ -24,8 +24,8 @@ http {
default_type application/octet-stream; default_type application/octet-stream;
add_header X-Content-Type-Options nosniff; add_header X-Content-Type-Options nosniff;
access_log /dev/stdout private; access_log /var/log/nginx/access.log private;
error_log /dev/stderr; error_log /var/log/nginx/error.log;
add_header Referrer-Policy same-origin; add_header Referrer-Policy same-origin;
gzip on; gzip on;

View File

@@ -5,8 +5,6 @@ export DATA_DIR=/data/
export HOME=/pretix export HOME=/pretix
export NUM_WORKERS=$((2 * $(nproc --all))) export NUM_WORKERS=$((2 * $(nproc --all)))
AUTOMIGRATE=${AUTOMIGRATE:-yes}
if [ ! -d /data/logs ]; then if [ ! -d /data/logs ]; then
mkdir /data/logs; mkdir /data/logs;
fi fi
@@ -18,16 +16,10 @@ if [ "$1" == "cron" ]; then
exec python3 -m pretix runperiodic exec python3 -m pretix runperiodic
fi fi
if [ "$AUTOMIGRATE" != "skip" ]; then python3 -m pretix migrate --noinput
python3 -m pretix migrate --noinput
fi
if [ "$1" == "all" ]; then if [ "$1" == "all" ]; then
exec sudo -E /usr/bin/supervisord -n -c /etc/supervisord.all.conf exec sudo -E /usr/bin/supervisord -n -c /etc/supervisord.conf
fi
if [ "$1" == "web" ]; then
exec sudo -E /usr/bin/supervisord -n -c /etc/supervisord.web.conf
fi fi
if [ "$1" == "webworker" ]; then if [ "$1" == "webworker" ]; then
@@ -45,6 +37,10 @@ if [ "$1" == "taskworker" ]; then
exec celery -A pretix.celery_app worker -l info "$@" exec celery -A pretix.celery_app worker -l info "$@"
fi fi
if [ "$1" == "shell" ]; then
exec python3 -m pretix shell
fi
if [ "$1" == "upgrade" ]; then if [ "$1" == "upgrade" ]; then
exec python3 -m pretix updatestyles exec python3 -m pretix updatestyles
fi fi

View File

@@ -1,2 +0,0 @@
[include]
files = /etc/supervisord/*.conf

View File

@@ -0,0 +1,44 @@
[unix_http_server]
file=/tmp/supervisor.sock
[supervisord]
logfile=/tmp/supervisord.log
logfile_maxbytes=50MB
logfile_backups=10
loglevel=info
pidfile=/tmp/supervisord.pid
nodaemon=false
minfds=1024
minprocs=200
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[supervisorctl]
serverurl=unix:///tmp/supervisor.sock
[program:pretixweb]
command=/usr/local/bin/pretix webworker
autostart=true
autorestart=true
priority=5
user=pretixuser
environment=HOME=/pretix
[program:pretixtask]
command=/usr/local/bin/pretix taskworker
autostart=true
autorestart=true
priority=5
user=pretixuser
[program:nginx]
command=/usr/sbin/nginx
autostart=true
autorestart=true
priority=10
stdout_events_enabled=true
stderr_events_enabled=true
[include]
files = /etc/supervisord-*.conf

View File

@@ -1,2 +0,0 @@
[include]
files = /etc/supervisord/base.conf /etc/supervisord/nginx.conf /etc/supervisord/pretixweb.conf

View File

@@ -1,18 +0,0 @@
[unix_http_server]
file=/tmp/supervisor.sock
[supervisord]
logfile=/dev/stderr
logfile_maxbytes=0
logfile_backups=10
loglevel=info
pidfile=/tmp/supervisord.pid
nodaemon=false
minfds=1024
minprocs=200
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[supervisorctl]
serverurl=unix:///tmp/supervisor.sock

View File

@@ -1,11 +0,0 @@
[program:nginx]
command=/usr/sbin/nginx
autostart=true
autorestart=true
priority=10
stdout_events_enabled=true
stderr_events_enabled=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

View File

@@ -1,10 +0,0 @@
[program:pretixtask]
command=/usr/local/bin/pretix taskworker
autostart=true
autorestart=true
priority=5
user=pretixuser
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

View File

@@ -1,11 +0,0 @@
[program:pretixweb]
command=/usr/local/bin/pretix webworker
autostart=true
autorestart=true
priority=5
user=pretixuser
environment=HOME=/pretix
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

View File

@@ -23,14 +23,6 @@ The config file may contain the following sections (all settings are optional an
default values). We suggest that you start from the examples given in one of the default values). We suggest that you start from the examples given in one of the
installation tutorials. installation tutorials.
.. note::
The configuration file is the recommended way to configure pretix. However, you can
also set them through environment variables. In this case, the syntax is
``PRETIX_SECTION_CONFIG``. For example, to configure the setting ``password_reset``
from the ``[pretix]`` section, set ``PRETIX_PRETIX_PASSWORD_RESET=off`` in your
environment.
pretix settings pretix settings
--------------- ---------------

View File

@@ -284,24 +284,6 @@ Then, go to that directory and build the image::
You can now use that image ``mypretix`` instead of ``pretix/standalone`` in your service file (see above). Be sure You can now use that image ``mypretix`` instead of ``pretix/standalone`` in your service file (see above). Be sure
to re-build your custom image after you pulled ``pretix/standalone`` if you want to perform an update. to re-build your custom image after you pulled ``pretix/standalone`` if you want to perform an update.
Scaling up
----------
If you need to scale to multiple machines, please first read our :ref:`scaling guide <scaling>`.
If you run the official docker container on multiple machines, it is recommended to set the environment
variable ``AUTOMIGRATE=skip`` on all containers and run ``docker exec -it pretix.service pretix migrate``
on one machine after each upgrade manually, otherwise multiple containers might try to upgrade the
database schema at the same time.
To run only the ``pretix-web`` component of pretix as well as a nginx server serving static files, you
can invoke the container with ``docker run … pretix/standalone:stable web`` (instead of ``all``).
To run only ``pretix-worker``, you can run ``docker run … pretix/standalone:stable taskworker``. You can
also pass arguments to limit the worker to specific queues or to change the number of concurrent task
workers, e.g. ``docker run … taskworker -Q notifications --concurrency 32``.
.. _Docker: https://docs.docker.com/engine/installation/linux/debian/ .. _Docker: https://docs.docker.com/engine/installation/linux/debian/
.. _Postfix: https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-postfix-as-a-send-only-smtp-server-on-ubuntu-16-04 .. _Postfix: https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-postfix-as-a-send-only-smtp-server-on-ubuntu-16-04
.. _nginx: https://botleg.com/stories/https-with-lets-encrypt-and-nginx/ .. _nginx: https://botleg.com/stories/https-with-lets-encrypt-and-nginx/

View File

@@ -47,8 +47,6 @@ item_meta_properties object Item-specific m
valid_keys object Cryptographic keys for non-default signature schemes. valid_keys object Cryptographic keys for non-default signature schemes.
For performance reason, value is omitted in lists and For performance reason, value is omitted in lists and
only contained in detail views. Value can be cached. only contained in detail views. Value can be cached.
sales_channels list A list of sales channels this event is available for
sale on.
===================================== ========================== ======================================================= ===================================== ========================== =======================================================
@@ -93,11 +91,6 @@ sales_channels list A list of sales
The attribute ``valid_keys`` has been added. The attribute ``valid_keys`` has been added.
.. versionchanged:: 3.14
The attribute ``sales_channels`` has been added.
Endpoints Endpoints
--------- ---------
@@ -154,16 +147,11 @@ Endpoints
"timezone": "Europe/Berlin", "timezone": "Europe/Berlin",
"item_meta_properties": {}, "item_meta_properties": {},
"plugins": [ "plugins": [
"pretix.plugins.banktransfer", "pretix.plugins.banktransfer"
"pretix.plugins.stripe", "pretix.plugins.stripe"
"pretix.plugins.paypal", "pretix.plugins.paypal"
"pretix.plugins.ticketoutputpdf" "pretix.plugins.ticketoutputpdf"
], ],
"sales_channels": [
"web",
"pretixpos",
"resellers"
]
} }
] ]
} }
@@ -182,7 +170,6 @@ Endpoints
only contain the events matching the set criteria. Providing ``?attr[Format]=Seminar`` would return only those only contain the events matching the set criteria. Providing ``?attr[Format]=Seminar`` would return only those
events having set their ``Format`` meta data to ``Seminar``, ``?attr[Format]=`` only those, that have no value events having set their ``Format`` meta data to ``Seminar``, ``?attr[Format]=`` only those, that have no value
set. Please note that this filter will respect default values set on organizer level. set. Please note that this filter will respect default values set on organizer level.
:query sales_channel: If set to a sales channel identifier, only events allowed to be sold on the specified sales channel are returned.
:param organizer: The ``slug`` field of a valid organizer :param organizer: The ``slug`` field of a valid organizer
:statuscode 200: no error :statuscode 200: no error
:statuscode 401: Authentication failure :statuscode 401: Authentication failure
@@ -232,21 +219,16 @@ Endpoints
"timezone": "Europe/Berlin", "timezone": "Europe/Berlin",
"item_meta_properties": {}, "item_meta_properties": {},
"plugins": [ "plugins": [
"pretix.plugins.banktransfer", "pretix.plugins.banktransfer"
"pretix.plugins.stripe", "pretix.plugins.stripe"
"pretix.plugins.paypal", "pretix.plugins.paypal"
"pretix.plugins.ticketoutputpdf" "pretix.plugins.ticketoutputpdf"
], ],
"valid_keys": { "valid_keys": {
"pretix_sig1": [ "pretix_sig1": [
"LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUNvd0JRWURLMlZ3QXlFQTdBRDcvdkZBMzNFc1k0ejJQSHI3aVpQc1o4bjVkaDBhalA4Z3l6Tm1tSXM9Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo=" "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUNvd0JRWURLMlZ3QXlFQTdBRDcvdkZBMzNFc1k0ejJQSHI3aVpQc1o4bjVkaDBhalA4Z3l6Tm1tSXM9Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo="
] ]
}, }
"sales_channels": [
"web",
"pretixpos",
"resellers"
]
} }
:param organizer: The ``slug`` field of the organizer to fetch :param organizer: The ``slug`` field of the organizer to fetch
@@ -297,11 +279,6 @@ Endpoints
"plugins": [ "plugins": [
"pretix.plugins.stripe", "pretix.plugins.stripe",
"pretix.plugins.paypal" "pretix.plugins.paypal"
],
"sales_channels": [
"web",
"pretixpos",
"resellers"
] ]
} }
@@ -337,11 +314,6 @@ Endpoints
"plugins": [ "plugins": [
"pretix.plugins.stripe", "pretix.plugins.stripe",
"pretix.plugins.paypal" "pretix.plugins.paypal"
],
"sales_channels": [
"web",
"pretixpos",
"resellers"
] ]
} }
@@ -397,11 +369,6 @@ Endpoints
"plugins": [ "plugins": [
"pretix.plugins.stripe", "pretix.plugins.stripe",
"pretix.plugins.paypal" "pretix.plugins.paypal"
],
"sales_channels": [
"web",
"pretixpos",
"resellers"
] ]
} }
@@ -437,11 +404,6 @@ Endpoints
"plugins": [ "plugins": [
"pretix.plugins.stripe", "pretix.plugins.stripe",
"pretix.plugins.paypal" "pretix.plugins.paypal"
],
"sales_channels": [
"web",
"pretixpos",
"resellers"
] ]
} }
@@ -511,11 +473,6 @@ Endpoints
"pretix.plugins.stripe", "pretix.plugins.stripe",
"pretix.plugins.paypal", "pretix.plugins.paypal",
"pretix.plugins.pretixdroid" "pretix.plugins.pretixdroid"
],
"sales_channels": [
"web",
"pretixpos",
"resellers"
] ]
} }

View File

@@ -1,215 +0,0 @@
.. spelling:: checkin
Data exporters
==============
pretix and it's plugins include a number of data exporters that allow you to bulk download various data from pretix in
different formats. This page shows you how to use these exporters through the API.
.. versionchanged:: 3.13
This feature has been added to the API.
.. warning::
While we consider the methods listed on this page to be a stable API, the availability and specific input field
requirements of individual exporters is **not considered a stable API**. Specific exporters and their input parameters
may change at any time without warning.
Listing available exporters
---------------------------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/exporters/
Returns a list of all exporters available for a given event. You will receive a list of export methods as well as their
supported input fields. Note that the exact type and validation requirements of the input fields are not given in the
response, and you might need to look into the pretix web interface to figure out the exact input required.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/exporters/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"identifier": "orderlist",
"verbose_name": "Order data",
"input_parameters": [
{
"name": "_format",
"required": true,
"choices": [
"xlsx",
"orders:default",
"orders:excel",
"orders:semicolon",
"positions:default",
"positions:excel",
"positions:semicolon",
"fees:default",
"fees:excel",
"fees:semicolon"
]
},
{
"name": "paid_only",
"required": false
}
]
}
]
}
:query integer page: The page number in case of a multi-page result set, default is 1
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/exporters/
Returns a list of all cross-event exporters available for a given organizer. You will receive a list of export methods as well as their
supported input fields. Note that the exact type and validation requirements of the input fields are not given in the
response, and you might need to look into the pretix web interface to figure out the exact input required.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/exporters/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"identifier": "orderlist",
"verbose_name": "Order data",
"input_parameters": [
{
"name": "events",
"required": true
},
{
"name": "_format",
"required": true,
"choices": [
"xlsx",
"orders:default",
"orders:excel",
"orders:semicolon",
"positions:default",
"positions:excel",
"positions:semicolon",
"fees:default",
"fees:excel",
"fees:semicolon"
]
},
{
"name": "paid_only",
"required": false
}
]
}
]
}
:query integer page: The page number in case of a multi-page result set, default is 1
:param organizer: The ``slug`` field of the organizer to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
Running an export
-----------------
Since exports often include large data sets, they might take longer than the duration of an HTTP request. Therefore,
creating an export is a two-step process. First you need to start an export task with one of the following to API
endpoints:
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/exporters/(identifier)/run/
Starts an export task. If your input parameters validate correctly, a ``202 Accepted`` status code is returned.
The body points you to the download URL of the result.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/exporters/orderlist/run/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"_format": "xlsx"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"download": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/orderlist/download/29891ede-196f-4942-9e26-d055a36e98b8/3f279f13-c198-4137-b49b-9b360ce9fcce/"
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param identifier: The ``identifier`` field of the exporter to run
:statuscode 202: no error
:statuscode 400: Invalid input options
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:post:: /api/v1/organizers/(organizer)/exporters/(identifier)/run/
The endpoint for organizer-level exports works just like event-level exports (see above).
Downloading the result
----------------------
When starting an export, you receive a ``url`` for downloading the result. Running a ``GET`` request on that result will
yield one of the following status codes:
* ``200 OK`` The export succeeded. The body will be your resulting file. Might be large!
* ``409 Conflict`` Your export is still running. The body will be JSON with the structure ``{"status": "running", "percentage": 40}``. ``percentage`` can be ``null`` if it is not known and ``status`` can be ``waiting`` before the task is actually being processed. Please retry, but wait at least one second before you do.
* ``410 Gone`` Running the export has failed permanently. The body will be JSON with the structure ``{"status": "failed", "message": "Error message"}``
* ``404 Not Found`` The export does not exist / is expired.
.. warning::
Running exports puts a lot of stress on the system, we kindly ask you not to run more than two exports at the same time.

View File

@@ -22,28 +22,9 @@ expires datetime Expiry date (or
conditions string Special terms and conditions for this card (or ``null``) conditions string Special terms and conditions for this card (or ``null``)
===================================== ========================== ======================================================= ===================================== ========================== =======================================================
The gift card transaction resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the gift card transaction
datetime datetime Creation date of the transaction
value money (string) Transaction amount
event string Event slug, if the gift card was used in the web shop (or ``null``)
order string Order code, if the gift card was used in the web shop (or ``null``)
text string Custom text of the transaction (or ``null``)
===================================== ========================== =======================================================
Endpoints Endpoints
--------- ---------
.. versionadded:: 3.14
The transaction list endpoint was added.
.. http:get:: /api/v1/organizers/(organizer)/giftcards/ .. http:get:: /api/v1/organizers/(organizer)/giftcards/
Returns a list of all gift cards issued by a given organizer. Returns a list of all gift cards issued by a given organizer.
@@ -269,45 +250,3 @@ Endpoints
:statuscode 401: Authentication failure :statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource. :statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource.
:statuscode 409: There is not sufficient credit on the gift card. :statuscode 409: There is not sufficient credit on the gift card.
.. http:get:: /api/v1/organizers/(organizer)/giftcards/(id)/transactions/
List all transactions of a gift card.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/giftcards/1/transactions/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 82,
"datetime": "2020-06-22T15:41:42.800534Z",
"value": "50.00",
"event": "democon",
"order": "FXQYW",
"text": null
}
]
}
:param organizer: The ``slug`` field of the organizer to view
:param id: The ``id`` field of the gift card to view
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.

View File

@@ -27,6 +27,5 @@ Resources and endpoints
devices devices
webhooks webhooks
seatingplans seatingplans
exporters
billing_invoices billing_invoices
billing_var billing_var

View File

@@ -163,10 +163,6 @@ last_modified datetime Last modificati
The ``exclude`` and ``subevent_after`` query parameter has been added. The ``exclude`` and ``subevent_after`` query parameter has been added.
.. versionchanged:: 3.13
The ``subevent_before`` query parameter has been added.
.. _order-position-resource: .. _order-position-resource:
@@ -494,8 +490,7 @@ List of all orders
recommend using this in combination with ``testmode=false``, since test mode orders can vanish at any time and recommend using this in combination with ``testmode=false``, since test mode orders can vanish at any time and
you will not notice it using this method. you will not notice it using this method.
:query datetime created_since: Only return orders that have been created since the given date. :query datetime created_since: Only return orders that have been created since the given date.
:query datetime subevent_after: Only return orders that contain a ticket for a subevent taking place after the given date. This is an exclusive after, and it considers the **end** of the subevent (or its start, if the end is not set). :query datetime subevent_after: Only return orders that contain a ticket for a subevent taking place after the given date.
:query datetime subevent_before: Only return orders that contain a ticket for a subevent taking place after the given date. This is an exclusive before, and it considers the **start** of the subevent.
:query string exclude: Exclude a field from the output, e.g. ``fees`` or ``positions.downloads``. Can be used as a performance optimization. Can be passed multiple times. :query string exclude: Exclude a field from the output, e.g. ``fees`` or ``positions.downloads``. Can be used as a performance optimization. Can be passed multiple times.
:param organizer: The ``slug`` field of the organizer to fetch :param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch :param event: The ``slug`` field of the event to fetch
@@ -940,9 +935,9 @@ Creating orders
during order generation and is not respected automatically when the order changes later.) during order generation and is not respected automatically when the order changes later.)
* ``force`` (optional). If set to ``true``, quotas will be ignored. * ``force`` (optional). If set to ``true``, quotas will be ignored.
* ``send_email`` (optional). If set to ``true``, the same emails will be sent as for a regular order, regardless of * ``send_mail`` (optional). If set to ``true``, the same emails will be sent as for a regular order, regardless of
whether these emails are enabled for certain sales channels. Defaults to whether these emails are enabled for certain sales channels. Defaults to
``false``. Used to be ``send_mail`` before pretix 3.14. ``false``.
If you want to use add-on products, you need to set the ``positionid`` fields of all positions manually If you want to use add-on products, you need to set the ``positionid`` fields of all positions manually
to incrementing integers starting with ``1``. Then, you can reference one of these to incrementing integers starting with ``1``. Then, you can reference one of these
@@ -1976,7 +1971,6 @@ Order payment endpoints
"amount": "23.00", "amount": "23.00",
"payment_date": "2017-12-04T12:13:12Z", "payment_date": "2017-12-04T12:13:12Z",
"info": {}, "info": {},
"send_email": false,
"provider": "banktransfer" "provider": "banktransfer"
} }

View File

@@ -90,120 +90,3 @@ Endpoints
:statuscode 200: no error :statuscode 200: no error
:statuscode 401: Authentication failure :statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it. :statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
Organizer settings
------------------
pretix organizers and events have lots and lots of parameters of different types that are stored in a key-value store on our system.
Since many of these settings depend on each other in complex ways, we can not give direct access to all of these
settings through the API. However, we do expose many of the simple and useful flags through the API.
Please note that the available settings flags change between pretix versions, and we do not give a guarantee on backwards-compatibility like with other parts of the API.
Therefore, we're also not including a list of the options here, but instead recommend to look at the endpoint output
to see available options. The ``explain=true`` flag enables a verbose mode that provides you with human-readable
information about the properties.
.. note:: Please note that this is not a complete representation of all organizer settings. You will find more settings
in the web interface.
.. warning:: This API is intended for advanced users. Even though we take care to validate your input, you will be
able to break your shops using this API by creating situations of conflicting settings. Please take care.
.. versionchanged:: 3.14
Initial support for settings has been added to the API.
.. http:get:: /api/v1/organizers/(organizer)/settings/
Get current values of organizer settings.
Permission required: "Can change organizer settings"
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/settings/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example standard response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"event_list_type": "calendar",
}
**Example verbose response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"event_list_type":
{
"value": "calendar",
"label": "Default overview style",
"help_text": "If your event series has more than 50 dates in the future, only the month or week calendar can be used."
}
},
}
:param organizer: The ``slug`` field of the organizer to access
:query explain: Set to ``true`` to enable verbose response mode
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
.. http:patch:: /api/v1/organizers/(organizer)/settings/
Updates organizer settings. Note that ``PUT`` is not allowed here, only ``PATCH``.
.. warning::
Settings can be stored at different levels in pretix. If a value is not set on organizer level, a default setting
from a higher level (global) will be returned. If you explicitly set a setting on organizer level, it
will no longer be inherited from the higher levels. Therefore, we recommend you to send only settings that you
explicitly want to set on organizer level. To unset a settings, pass ``null``.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/settings/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"event_list_type": "calendar"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"event_list_type": "calendar",
}
:param organizer: The ``slug`` field of the organizer to update
:statuscode 200: no error
:statuscode 400: The organizer could not be updated due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.

View File

@@ -1,7 +1,4 @@
.. spelling:: .. spelling:: checkin
checkin
datetime
.. _rest-questions: .. _rest-questions:
@@ -56,12 +53,6 @@ options list of objects In case of ques
├ identifier string An arbitrary string that can be used for matching with ├ identifier string An arbitrary string that can be used for matching with
other sources. other sources.
└ answer multi-lingual string The displayed value of this option └ answer multi-lingual string The displayed value of this option
valid_number_min string Minimum value for number questions (optional)
valid_number_max string Maximum value for number questions (optional)
valid_date_min date Minimum value for date questions (optional)
valid_date_max date Maximum value for date questions (optional)
valid_datetime_min datetime Minimum value for date and time questions (optional)
valid_datetime_max datetime Maximum value for date and time questions (optional)
dependency_question integer Internal ID of a different question. The current dependency_question integer Internal ID of a different question. The current
question will only be shown if the question given in question will only be shown if the question given in
this attribute is set to the value given in this attribute is set to the value given in
@@ -101,10 +92,6 @@ dependency_value string An old version
The attribute ``help_text`` has been added. The attribute ``help_text`` has been added.
.. versionchanged:: 3.14
The attributes ``valid_*`` have been added.
Endpoints Endpoints
--------- ---------
@@ -150,12 +137,6 @@ Endpoints
"ask_during_checkin": false, "ask_during_checkin": false,
"hidden": false, "hidden": false,
"print_on_invoice": false, "print_on_invoice": false,
"valid_number_min": null,
"valid_number_max": null,
"valid_date_min": null,
"valid_date_max": null,
"valid_datetime_min": null,
"valid_datetime_max": null,
"dependency_question": null, "dependency_question": null,
"dependency_value": null, "dependency_value": null,
"dependency_values": [], "dependency_values": [],
@@ -227,12 +208,6 @@ Endpoints
"ask_during_checkin": false, "ask_during_checkin": false,
"hidden": false, "hidden": false,
"print_on_invoice": false, "print_on_invoice": false,
"valid_number_min": null,
"valid_number_max": null,
"valid_date_min": null,
"valid_date_max": null,
"valid_datetime_min": null,
"valid_datetime_max": null,
"dependency_question": null, "dependency_question": null,
"dependency_value": null, "dependency_value": null,
"dependency_values": [], "dependency_values": [],
@@ -327,12 +302,6 @@ Endpoints
"dependency_question": null, "dependency_question": null,
"dependency_value": null, "dependency_value": null,
"dependency_values": [], "dependency_values": [],
"valid_number_min": null,
"valid_number_max": null,
"valid_date_min": null,
"valid_date_max": null,
"valid_datetime_min": null,
"valid_datetime_max": null,
"options": [ "options": [
{ {
"id": 1, "id": 1,
@@ -408,12 +377,6 @@ Endpoints
"dependency_question": null, "dependency_question": null,
"dependency_value": null, "dependency_value": null,
"dependency_values": [], "dependency_values": [],
"valid_number_min": null,
"valid_number_max": null,
"valid_date_min": null,
"valid_date_max": null,
"valid_datetime_min": null,
"valid_datetime_max": null,
"options": [ "options": [
{ {
"id": 1, "id": 1,

View File

@@ -31,10 +31,8 @@ action_types list of strings A list of actio
The following values for ``action_types`` are valid with pretix core: The following values for ``action_types`` are valid with pretix core:
* ``pretix.event.order.placed`` * ``pretix.event.order.placed``
* ``pretix.event.order.placed.require_approval``
* ``pretix.event.order.paid`` * ``pretix.event.order.paid``
* ``pretix.event.order.canceled`` * ``pretix.event.order.canceled``
* ``pretix.event.order.reactivated``
* ``pretix.event.order.expired`` * ``pretix.event.order.expired``
* ``pretix.event.order.modified`` * ``pretix.event.order.modified``
* ``pretix.event.order.contact.changed`` * ``pretix.event.order.contact.changed``
@@ -44,12 +42,6 @@ The following values for ``action_types`` are valid with pretix core:
* ``pretix.event.order.denied`` * ``pretix.event.order.denied``
* ``pretix.event.checkin`` * ``pretix.event.checkin``
* ``pretix.event.checkin.reverted`` * ``pretix.event.checkin.reverted``
* ``pretix.event.added``
* ``pretix.event.changed``
* ``pretix.event.deleted``
* ``pretix.subevent.added``
* ``pretix.subevent.changed``
* ``pretix.subevent.deleted``
Installed plugins might register more valid values. Installed plugins might register more valid values.

View File

@@ -58,7 +58,7 @@ Backend
.. automodule:: pretix.control.signals .. automodule:: pretix.control.signals
:members: nav_event, html_head, html_page_start, quota_detail_html, nav_topbar, nav_global, nav_organizer, nav_event_settings, :members: nav_event, html_head, html_page_start, quota_detail_html, nav_topbar, nav_global, nav_organizer, nav_event_settings,
order_info, event_settings_widget, oauth_application_registered, order_position_buttons, subevent_forms, order_info, event_settings_widget, oauth_application_registered, order_position_buttons, subevent_forms,
item_formsets, order_search_filter_q, order_search_forms item_formsets, order_search_filter_q
.. automodule:: pretix.base.signals .. automodule:: pretix.base.signals
:members: logentry_display, logentry_object_link, requiredaction_display, timeline_events :members: logentry_display, logentry_object_link, requiredaction_display, timeline_events

View File

@@ -1,11 +1,11 @@
include LICENSE include LICENSE
include README.rst include README.rst
global-include *.proto
recursive-include pretix/static * recursive-include pretix/static *
recursive-include pretix/static.dist * recursive-include pretix/static.dist *
recursive-include pretix/locale * recursive-include pretix/locale *
recursive-include pretix/helpers/locale * recursive-include pretix/helpers/locale *
recursive-include pretix/base/templates * recursive-include pretix/base/templates *
recursive-include pretix/base/secretgenerators *
recursive-include pretix/control/templates * recursive-include pretix/control/templates *
recursive-include pretix/presale/templates * recursive-include pretix/presale/templates *
recursive-include pretix/plugins/banktransfer/templates * recursive-include pretix/plugins/banktransfer/templates *

View File

@@ -7,7 +7,7 @@ localecompile:
localegen: localegen:
./manage.py makemessages --keep-pot --ignore "pretix/helpers/*" $(LNGS) ./manage.py makemessages --keep-pot --ignore "pretix/helpers/*" $(LNGS)
./manage.py makemessages --keep-pot -d djangojs --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "pretix/static/rrule/*" --ignore "build/*" $(LNGS) ./manage.py makemessages --keep-pot -d djangojs --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "build/*" $(LNGS)
staticfiles: jsi18n staticfiles: jsi18n
./manage.py collectstatic --noinput ./manage.py collectstatic --noinput

View File

@@ -1 +1 @@
__version__ = "3.14.0.dev0" __version__ = "3.12.0"

View File

@@ -102,17 +102,12 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
('DELETE', 'api-v1:cartposition-detail'), ('DELETE', 'api-v1:cartposition-detail'),
('GET', 'api-v1:giftcard-list'), ('GET', 'api-v1:giftcard-list'),
('POST', 'api-v1:giftcard-transact'), ('POST', 'api-v1:giftcard-transact'),
('GET', 'plugins:pretix_posbackend:posclosing-list'),
('POST', 'plugins:pretix_posbackend:posreceipt-list'), ('POST', 'plugins:pretix_posbackend:posreceipt-list'),
('POST', 'plugins:pretix_posbackend:posclosing-list'), ('POST', 'plugins:pretix_posbackend:posclosing-list'),
('POST', 'plugins:pretix_posbackend:posdebugdump-list'), ('POST', 'plugins:pretix_posbackend:posdebugdump-list'),
('POST', 'plugins:pretix_posbackend:stripeterminal.token'), ('POST', 'plugins:pretix_posbackend:stripeterminal.token'),
('GET', 'api-v1:revokedsecrets-list'), ('GET', 'api-v1:revokedsecrets-list'),
('GET', 'api-v1:event.settings'), ('GET', 'api-v1:event.settings'),
('GET', 'plugins:pretix_seating:event.event'),
('GET', 'plugins:pretix_seating:event.event.subevent'),
('GET', 'plugins:pretix_seating:event.plan'),
('GET', 'plugins:pretix_seating:selection.simple'),
) )

View File

@@ -87,10 +87,7 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
raise ValidationError('The specified seat ID is not unique.') raise ValidationError('The specified seat ID is not unique.')
else: else:
validated_data['seat'] = seat validated_data['seat'] = seat
if not seat.is_available( if not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web')):
sales_channel=validated_data.get('sales_channel', 'web'),
distance_ignore_cart_id=validated_data['cart_id'],
):
raise ValidationError(gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)) raise ValidationError(gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name))
elif seated: elif seated:
raise ValidationError('The specified product requires to choose a seat.') raise ValidationError('The specified product requires to choose a seat.')
@@ -107,7 +104,6 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
def validate_cart_id(self, cid): def validate_cart_id(self, cid):
if cid and not cid.endswith('@api'): if cid and not cid.endswith('@api'):
raise ValidationError('Cart ID should end in @api or be empty.') raise ValidationError('Cart ID should end in @api or be empty.')
return cid
def validate_item(self, item): def validate_item(self, item):
if item.event != self.context['event']: if item.event != self.context['event']:

View File

@@ -17,7 +17,7 @@ from pretix.base.models.items import SubEventItem, SubEventItemVariation
from pretix.base.services.seating import ( from pretix.base.services.seating import (
SeatProtected, generate_seats, validate_plan_change, SeatProtected, generate_seats, validate_plan_change,
) )
from pretix.base.settings import DEFAULTS, validate_event_settings from pretix.base.settings import DEFAULTS, validate_settings
from pretix.base.signals import api_event_settings_fields from pretix.base.signals import api_event_settings_fields
@@ -124,8 +124,7 @@ class EventSerializer(I18nAwareModelSerializer):
fields = ('name', 'slug', 'live', 'testmode', 'currency', 'date_from', fields = ('name', 'slug', 'live', 'testmode', 'currency', 'date_from',
'date_to', 'date_admission', 'is_public', 'presale_start', 'date_to', 'date_admission', 'is_public', 'presale_start',
'presale_end', 'location', 'geo_lat', 'geo_lon', 'has_subevents', 'meta_data', 'seating_plan', 'presale_end', 'location', 'geo_lat', 'geo_lon', 'has_subevents', 'meta_data', 'seating_plan',
'plugins', 'seat_category_mapping', 'timezone', 'item_meta_properties', 'valid_keys', 'plugins', 'seat_category_mapping', 'timezone', 'item_meta_properties', 'valid_keys')
'sales_channels')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@@ -597,7 +596,6 @@ class EventSettingsSerializer(serializers.Serializer):
'attendee_addresses_required', 'attendee_addresses_required',
'attendee_company_asked', 'attendee_company_asked',
'attendee_company_required', 'attendee_company_required',
'attendee_data_explanation_text',
'confirm_texts', 'confirm_texts',
'order_email_asked_twice', 'order_email_asked_twice',
'payment_term_mode', 'payment_term_mode',
@@ -608,7 +606,6 @@ class EventSettingsSerializer(serializers.Serializer):
'payment_term_expire_automatically', 'payment_term_expire_automatically',
'payment_term_accept_late', 'payment_term_accept_late',
'payment_explanation', 'payment_explanation',
'payment_pending_hidden',
'ticket_download', 'ticket_download',
'ticket_download_date', 'ticket_download_date',
'ticket_download_addons', 'ticket_download_addons',
@@ -664,17 +661,10 @@ class EventSettingsSerializer(serializers.Serializer):
'change_allow_user_variation', 'change_allow_user_variation',
'change_allow_user_until', 'change_allow_user_until',
'change_allow_user_price', 'change_allow_user_price',
'primary_color',
'theme_color_success',
'theme_color_danger',
'theme_color_background',
'theme_round_borders',
'primary_font',
] ]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.event = kwargs.pop('event') self.event = kwargs.pop('event')
self.changed_data = []
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
for fname in self.default_fields: for fname in self.default_fields:
kwargs = DEFAULTS[fname].get('serializer_kwargs', {}) kwargs = DEFAULTS[fname].get('serializer_kwargs', {})
@@ -703,17 +693,15 @@ class EventSettingsSerializer(serializers.Serializer):
for attr, value in validated_data.items(): for attr, value in validated_data.items():
if value is None: if value is None:
instance.delete(attr) instance.delete(attr)
self.changed_data.append(attr)
elif instance.get(attr, as_type=type(value)) != value: elif instance.get(attr, as_type=type(value)) != value:
instance.set(attr, value) instance.set(attr, value)
self.changed_data.append(attr)
return instance return instance
def validate(self, data): def validate(self, data):
data = super().validate(data) data = super().validate(data)
settings_dict = self.instance.freeze() settings_dict = self.instance.freeze()
settings_dict.update(data) settings_dict.update(data)
validate_event_settings(self.event, settings_dict) validate_settings(self.event, settings_dict)
return data return data

View File

@@ -1,127 +0,0 @@
from django import forms
from django.http import QueryDict
from rest_framework import serializers
class FormFieldWrapperField(serializers.Field):
def __init__(self, *args, **kwargs):
self.form_field = kwargs.pop('form_field')
super().__init__(*args, **kwargs)
def to_representation(self, value):
return self.form_field.widget.format_value(value)
def to_internal_value(self, data):
d = self.form_field.widget.value_from_datadict({'name': data}, {}, 'name')
d = self.form_field.clean(d)
return d
simple_mappings = (
(forms.DateField, serializers.DateField, tuple()),
(forms.TimeField, serializers.TimeField, tuple()),
(forms.SplitDateTimeField, serializers.DateTimeField, tuple()),
(forms.DateTimeField, serializers.DateTimeField, tuple()),
(forms.DecimalField, serializers.DecimalField, ('max_digits', 'decimal_places', 'min_value', 'max_value')),
(forms.FloatField, serializers.FloatField, tuple()),
(forms.IntegerField, serializers.IntegerField, tuple()),
(forms.EmailField, serializers.EmailField, tuple()),
(forms.UUIDField, serializers.UUIDField, tuple()),
(forms.URLField, serializers.URLField, tuple()),
(forms.NullBooleanField, serializers.NullBooleanField, tuple()),
(forms.BooleanField, serializers.BooleanField, tuple()),
)
class SerializerDescriptionField(serializers.Field):
def to_representation(self, value):
fields = []
for k, v in value.fields.items():
d = {
'name': k,
'required': v.required,
}
if isinstance(v, serializers.ChoiceField):
d['choices'] = list(v.choices.keys())
fields.append(d)
return fields
class ExporterSerializer(serializers.Serializer):
identifier = serializers.CharField()
verbose_name = serializers.CharField()
input_parameters = SerializerDescriptionField(source='_serializer')
class PrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
def to_representation(self, value):
if isinstance(value, int):
return value
return super().to_representation(value)
class JobRunSerializer(serializers.Serializer):
def __init__(self, *args, **kwargs):
ex = kwargs.pop('exporter')
events = kwargs.pop('events', None)
super().__init__(*args, **kwargs)
if events is not None:
self.fields["events"] = serializers.SlugRelatedField(
queryset=events,
required=True,
allow_empty=False,
slug_field='slug',
many=True
)
for k, v in ex.export_form_fields.items():
for m_from, m_to, m_kwargs in simple_mappings:
if isinstance(v, m_from):
self.fields[k] = m_to(
required=v.required,
allow_null=not v.required,
validators=v.validators,
**{kwarg: getattr(v, kwargs, None) for kwarg in m_kwargs}
)
break
if isinstance(v, forms.ModelMultipleChoiceField):
self.fields[k] = PrimaryKeyRelatedField(
queryset=v.queryset,
required=v.required,
allow_empty=not v.required,
validators=v.validators,
many=True
)
elif isinstance(v, forms.ModelChoiceField):
self.fields[k] = PrimaryKeyRelatedField(
queryset=v.queryset,
required=v.required,
allow_null=not v.required,
validators=v.validators,
)
elif isinstance(v, forms.MultipleChoiceField):
self.fields[k] = serializers.MultipleChoiceField(
choices=v.choices,
required=v.required,
allow_empty=not v.required,
validators=v.validators,
)
elif isinstance(v, forms.ChoiceField):
self.fields[k] = serializers.ChoiceField(
choices=v.choices,
required=v.required,
allow_null=not v.required,
validators=v.validators,
)
else:
self.fields[k] = FormFieldWrapperField(form_field=v, required=v.required, allow_null=not v.required)
def to_internal_value(self, data):
if isinstance(data, QueryDict):
data = data.copy()
for k, v in self.fields.items():
if isinstance(v, serializers.ManyRelatedField) and k not in data:
data[k] = []
data = super().to_internal_value(data)
return data

View File

@@ -277,9 +277,7 @@ class QuestionSerializer(I18nAwareModelSerializer):
model = Question model = Question
fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position', fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position',
'ask_during_checkin', 'identifier', 'dependency_question', 'dependency_values', 'ask_during_checkin', 'identifier', 'dependency_question', 'dependency_values',
'hidden', 'dependency_value', 'print_on_invoice', 'help_text', 'valid_number_min', 'hidden', 'dependency_value', 'print_on_invoice', 'help_text')
'valid_number_max', 'valid_date_min', 'valid_date_max', 'valid_datetime_min', 'valid_datetime_max'
)
def validate_identifier(self, value): def validate_identifier(self, value):
Question._clean_identifier(self.context['event'], value, self.instance) Question._clean_identifier(self.context['event'], value, self.instance)

View File

@@ -682,7 +682,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
consume_carts = serializers.ListField(child=serializers.CharField(), required=False) consume_carts = serializers.ListField(child=serializers.CharField(), required=False)
force = serializers.BooleanField(default=False, required=False) force = serializers.BooleanField(default=False, required=False)
payment_date = serializers.DateTimeField(required=False, allow_null=True) payment_date = serializers.DateTimeField(required=False, allow_null=True)
send_email = serializers.BooleanField(default=False, required=False) send_mail = serializers.BooleanField(default=False, required=False)
simulate = serializers.BooleanField(default=False, required=False) simulate = serializers.BooleanField(default=False, required=False)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@@ -693,7 +693,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
model = Order model = Order
fields = ('code', 'status', 'testmode', 'email', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel', fields = ('code', 'status', 'testmode', 'email', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts', 'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts',
'force', 'send_email', 'simulate') 'force', 'send_mail', 'simulate')
def validate_payment_provider(self, pp): def validate_payment_provider(self, pp):
if pp is None: if pp is None:
@@ -786,7 +786,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
payment_date = validated_data.pop('payment_date', now()) payment_date = validated_data.pop('payment_date', now())
force = validated_data.pop('force', False) force = validated_data.pop('force', False)
simulate = validated_data.pop('simulate', False) simulate = validated_data.pop('simulate', False)
self._send_mail = validated_data.pop('send_email', False) self._send_mail = validated_data.pop('send_mail', False)
if 'invoice_address' in validated_data: if 'invoice_address' in validated_data:
iadata = validated_data.pop('invoice_address') iadata = validated_data.pop('invoice_address')

View File

@@ -2,7 +2,6 @@ from decimal import Decimal
from django.db.models import Q from django.db.models import Q
from django.utils.translation import get_language, gettext_lazy as _ from django.utils.translation import get_language, gettext_lazy as _
from hierarkey.proxy import HierarkeyProxy
from rest_framework import serializers from rest_framework import serializers
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
@@ -10,12 +9,11 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.order import CompatibleJSONField from pretix.api.serializers.order import CompatibleJSONField
from pretix.base.auth import get_auth_backends from pretix.base.auth import get_auth_backends
from pretix.base.models import ( from pretix.base.models import (
Device, GiftCard, GiftCardTransaction, Organizer, SeatingPlan, Team, Device, GiftCard, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite,
TeamAPIToken, TeamInvite, User, User,
) )
from pretix.base.models.seating import SeatingPlanLayoutValidator from pretix.base.models.seating import SeatingPlanLayoutValidator
from pretix.base.services.mail import SendMailException, mail from pretix.base.services.mail import SendMailException, mail
from pretix.base.settings import DEFAULTS, validate_organizer_settings
from pretix.helpers.urls import build_absolute_uri from pretix.helpers.urls import build_absolute_uri
@@ -61,21 +59,6 @@ class GiftCardSerializer(I18nAwareModelSerializer):
fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode', 'expires', 'conditions') fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode', 'expires', 'conditions')
class OrderEventSlugField(serializers.RelatedField):
def to_representation(self, obj):
return obj.event.slug
class GiftCardTransactionSerializer(I18nAwareModelSerializer):
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
event = OrderEventSlugField(source='order', read_only=True)
class Meta:
model = GiftCardTransaction
fields = ('id', 'datetime', 'value', 'event', 'order', 'text')
class EventSlugField(serializers.SlugRelatedField): class EventSlugField(serializers.SlugRelatedField):
def get_queryset(self): def get_queryset(self):
return self.context['organizer'].events.all() return self.context['organizer'].events.all()
@@ -204,63 +187,3 @@ class TeamMemberSerializer(serializers.ModelSerializer):
fields = ( fields = (
'id', 'email', 'fullname', 'require_2fa' 'id', 'email', 'fullname', 'require_2fa'
) )
class OrganizerSettingsSerializer(serializers.Serializer):
default_fields = [
'organizer_info_text',
'event_list_type',
'event_list_availability',
'organizer_homepage_text',
'organizer_link_back',
'organizer_logo_image_large',
'giftcard_length',
'giftcard_expiry_years',
'locales',
'event_team_provisioning',
'primary_color',
'theme_color_success',
'theme_color_danger',
'theme_color_background',
'theme_round_borders',
'primary_font'
]
def __init__(self, *args, **kwargs):
self.organizer = kwargs.pop('organizer')
self.changed_data = []
super().__init__(*args, **kwargs)
for fname in self.default_fields:
kwargs = DEFAULTS[fname].get('serializer_kwargs', {})
if callable(kwargs):
kwargs = kwargs()
kwargs.setdefault('required', False)
kwargs.setdefault('allow_null', True)
form_kwargs = DEFAULTS[fname].get('form_kwargs', {})
if callable(form_kwargs):
form_kwargs = form_kwargs()
if 'serializer_class' not in DEFAULTS[fname]:
raise ValidationError('{} has no serializer class'.format(fname))
f = DEFAULTS[fname]['serializer_class'](
**kwargs
)
f._label = form_kwargs.get('label', fname)
f._help_text = form_kwargs.get('help_text')
self.fields[fname] = f
def update(self, instance: HierarkeyProxy, validated_data):
for attr, value in validated_data.items():
if value is None:
instance.delete(attr)
self.changed_data.append(attr)
elif instance.get(attr, as_type=type(value)) != value:
instance.set(attr, value)
self.changed_data.append(attr)
return instance
def validate(self, data):
data = super().validate(data)
settings_dict = self.instance.freeze()
settings_dict.update(data)
validate_organizer_settings(self.organizer, settings_dict)
return data

View File

@@ -7,8 +7,8 @@ from rest_framework import routers
from pretix.api.views import cart from pretix.api.views import cart
from .views import ( from .views import (
checkin, device, event, exporters, item, oauth, order, organizer, user, checkin, device, event, item, oauth, order, organizer, user, version,
version, voucher, waitinglist, webhooks, voucher, waitinglist, webhooks,
) )
router = routers.DefaultRouter() router = routers.DefaultRouter()
@@ -22,7 +22,6 @@ orga_router.register(r'seatingplans', organizer.SeatingPlanViewSet)
orga_router.register(r'giftcards', organizer.GiftCardViewSet) orga_router.register(r'giftcards', organizer.GiftCardViewSet)
orga_router.register(r'teams', organizer.TeamViewSet) orga_router.register(r'teams', organizer.TeamViewSet)
orga_router.register(r'devices', organizer.DeviceViewSet) orga_router.register(r'devices', organizer.DeviceViewSet)
orga_router.register(r'exporters', exporters.OrganizerExportersViewSet, basename='exporters')
team_router = routers.DefaultRouter() team_router = routers.DefaultRouter()
team_router.register(r'members', organizer.TeamMemberViewSet) team_router.register(r'members', organizer.TeamMemberViewSet)
@@ -45,7 +44,6 @@ event_router.register(r'taxrules', event.TaxRuleViewSet)
event_router.register(r'waitinglistentries', waitinglist.WaitingListViewSet) event_router.register(r'waitinglistentries', waitinglist.WaitingListViewSet)
event_router.register(r'checkinlists', checkin.CheckinListViewSet) event_router.register(r'checkinlists', checkin.CheckinListViewSet)
event_router.register(r'cartpositions', cart.CartPositionViewSet) event_router.register(r'cartpositions', cart.CartPositionViewSet)
event_router.register(r'exporters', exporters.EventExportersViewSet, basename='exporters')
checkinlist_router = routers.DefaultRouter() checkinlist_router = routers.DefaultRouter()
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet, basename='checkinlistpos') checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet, basename='checkinlistpos')
@@ -62,9 +60,6 @@ order_router = routers.DefaultRouter()
order_router.register(r'payments', order.PaymentViewSet) order_router.register(r'payments', order.PaymentViewSet)
order_router.register(r'refunds', order.RefundViewSet) order_router.register(r'refunds', order.RefundViewSet)
giftcard_router = routers.DefaultRouter()
giftcard_router.register(r'transactions', organizer.GiftCardTransactionViewSet)
# Force import of all plugins to give them a chance to register URLs with the router # Force import of all plugins to give them a chance to register URLs with the router
for app in apps.get_app_configs(): for app in apps.get_app_configs():
if hasattr(app, 'PretixPluginMeta'): if hasattr(app, 'PretixPluginMeta'):
@@ -74,9 +69,6 @@ for app in apps.get_app_configs():
urlpatterns = [ urlpatterns = [
url(r'^', include(router.urls)), url(r'^', include(router.urls)),
url(r'^organizers/(?P<organizer>[^/]+)/', include(orga_router.urls)), url(r'^organizers/(?P<organizer>[^/]+)/', include(orga_router.urls)),
url(r'^organizers/(?P<organizer>[^/]+)/settings/$', organizer.OrganizerSettingsView.as_view(),
name="organizer.settings"),
url(r'^organizers/(?P<organizer>[^/]+)/giftcards/(?P<giftcard>[^/]+)/', include(giftcard_router.urls)),
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/settings/$', event.EventSettingsView.as_view(), url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/settings/$', event.EventSettingsView.as_view(),
name="event.settings"), name="event.settings"),
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/', include(event_router.urls)), url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/', include(event_router.urls)),

View File

@@ -131,7 +131,7 @@ class EventSelectionView(APIView):
@property @property
def base_event_qs(self): def base_event_qs(self):
qs = self.request.auth.get_events_with_any_permission().annotate( qs = self.request.auth.organizer.events.annotate(
first_date=Coalesce('date_admission', 'date_from'), first_date=Coalesce('date_admission', 'date_from'),
last_date=Coalesce('date_to', 'date_from'), last_date=Coalesce('date_to', 'date_from'),
).filter( ).filter(
@@ -154,7 +154,6 @@ class EventSelectionView(APIView):
).filter( ).filter(
event__organizer=self.request.auth.organizer, event__organizer=self.request.auth.organizer,
event__live=True, event__live=True,
event__in=self.request.auth.get_events_with_any_permission(),
active=True, active=True,
).select_related('event').order_by('first_date') ).select_related('event').order_by('first_date')
if self.request.auth.gate: if self.request.auth.gate:

View File

@@ -18,9 +18,7 @@ from pretix.base.models import (
CartPosition, Device, Event, TaxRule, TeamAPIToken, CartPosition, Device, Event, TaxRule, TeamAPIToken,
) )
from pretix.base.models.event import SubEvent from pretix.base.models.event import SubEvent
from pretix.base.settings import SETTINGS_AFFECTING_CSS
from pretix.helpers.dicts import merge_dicts from pretix.helpers.dicts import merge_dicts
from pretix.presale.style import regenerate_css
from pretix.presale.views.organizer import filter_qs_by_attr from pretix.presale.views.organizer import filter_qs_by_attr
with scopes_disabled(): with scopes_disabled():
@@ -28,7 +26,6 @@ with scopes_disabled():
is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs') is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs')
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs') is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs') ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
sales_channel = django_filters.rest_framework.CharFilter(method='sales_channel_qs')
class Meta: class Meta:
model = Event model = Event
@@ -70,9 +67,6 @@ with scopes_disabled():
else: else:
return queryset.exclude(expr) return queryset.exclude(expr)
def sales_channel_qs(self, queryset, name, value):
return queryset.filter(sales_channels__contains=value)
class EventViewSet(viewsets.ModelViewSet): class EventViewSet(viewsets.ModelViewSet):
serializer_class = EventSerializer serializer_class = EventSerializer
@@ -391,7 +385,5 @@ class EventSettingsView(views.APIView):
k: v for k, v in s.validated_data.items() k: v for k, v in s.validated_data.items()
} }
) )
if any(p in s.changed_data for p in SETTINGS_AFFECTING_CSS):
regenerate_css.apply_async(args=(request.organizer.pk,))
s = EventSettingsSerializer(instance=request.event.settings, event=request.event) s = EventSettingsSerializer(instance=request.event.settings, event=request.event)
return Response(s.data) return Response(s.data)

View File

@@ -1,154 +0,0 @@
from datetime import timedelta
from celery.result import AsyncResult
from django.conf import settings
from django.http import Http404
from django.shortcuts import get_object_or_404
from django.utils.functional import cached_property
from django.utils.timezone import now
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.reverse import reverse
from pretix.api.serializers.exporters import (
ExporterSerializer, JobRunSerializer,
)
from pretix.base.models import CachedFile, Device, TeamAPIToken
from pretix.base.services.export import export, multiexport
from pretix.base.signals import (
register_data_exporters, register_multievent_data_exporters,
)
from pretix.helpers.http import ChunkBasedFileResponse
class ExportersMixin:
def list(self, request, *args, **kwargs):
res = ExporterSerializer(self.exporters, many=True)
return Response({
"count": len(self.exporters),
"next": None,
"previous": None,
"results": res.data
})
def get_object(self):
instances = [e for e in self.exporters if e.identifier == self.kwargs.get('pk')]
if not instances:
raise Http404()
return instances[0]
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
serializer = ExporterSerializer(instance)
return Response(serializer.data)
@action(detail=True, methods=['GET'], url_name='download', url_path='download/(?P<asyncid>[^/]+)/(?P<cfid>[^/]+)')
def download(self, *args, **kwargs):
cf = get_object_or_404(CachedFile, id=kwargs['cfid'])
if cf.file:
resp = ChunkBasedFileResponse(cf.file.file, content_type=cf.type)
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename)
return resp
elif not settings.HAS_CELERY:
return Response(
{'status': 'failed', 'message': 'Unknown file ID or export failed'},
status=status.HTTP_410_GONE
)
res = AsyncResult(kwargs['asyncid'])
if res.failed():
if isinstance(res.info, dict) and res.info['exc_type'] == 'ExportError':
msg = res.info['exc_message']
else:
msg = 'Internal error'
return Response(
{'status': 'failed', 'message': msg},
status=status.HTTP_410_GONE
)
return Response(
{
'status': 'running' if res.state in ('PROGRESS', 'STARTED', 'SUCCESS') else 'waiting',
'percentage': res.result.get('value', None) if res.result else None,
},
status=status.HTTP_409_CONFLICT
)
@action(detail=True, methods=['POST'])
def run(self, *args, **kwargs):
instance = self.get_object()
serializer = JobRunSerializer(exporter=instance, data=self.request.data, **self.get_serializer_kwargs())
serializer.is_valid(raise_exception=True)
cf = CachedFile()
cf.date = now()
cf.expires = now() + timedelta(days=3)
cf.save()
d = serializer.data
for k, v in d.items():
if isinstance(v, set):
d[k] = list(v)
async_result = self.do_export(cf, instance, d)
url_kwargs = {
'asyncid': str(async_result.id),
'cfid': str(cf.id),
}
url_kwargs.update(self.kwargs)
return Response({
'download': reverse('api-v1:exporters-download', kwargs=url_kwargs, request=self.request)
}, status=status.HTTP_202_ACCEPTED)
class EventExportersViewSet(ExportersMixin, viewsets.ViewSet):
permission = 'can_view_orders'
def get_serializer_kwargs(self):
return {}
@cached_property
def exporters(self):
exporters = []
responses = register_data_exporters.send(self.request.event)
for ex in sorted([response(self.request.event) for r, response in responses], key=lambda ex: str(ex.verbose_name)):
ex._serializer = JobRunSerializer(exporter=ex)
exporters.append(ex)
return exporters
def do_export(self, cf, instance, data):
return export.apply_async(args=(self.request.event.id, str(cf.id), instance.identifier, data))
class OrganizerExportersViewSet(ExportersMixin, viewsets.ViewSet):
permission = None
@cached_property
def exporters(self):
exporters = []
events = (self.request.auth or self.request.user).get_events_with_permission('can_view_orders', request=self.request).filter(
organizer=self.request.organizer
)
responses = register_multievent_data_exporters.send(self.request.organizer)
for ex in sorted([response(events) for r, response in responses if response], key=lambda ex: str(ex.verbose_name)):
ex._serializer = JobRunSerializer(exporter=ex, events=events)
exporters.append(ex)
return exporters
def get_serializer_kwargs(self):
return {
'events': self.request.auth.get_events_with_permission('can_view_orders', request=self.request).filter(
organizer=self.request.organizer
)
}
def do_export(self, cf, instance, data):
return multiexport.apply_async(kwargs={
'organizer': self.request.organizer.id,
'user': self.request.user.id if self.request.user.is_authenticated else None,
'token': self.request.auth.pk if isinstance(self.request.auth, TeamAPIToken) else None,
'device': self.request.auth.pk if isinstance(self.request.auth, Device) else None,
'fileid': str(cf.id),
'provider': instance.identifier,
'form_data': data
})

View File

@@ -33,7 +33,7 @@ from pretix.base.i18n import language
from pretix.base.models import ( from pretix.base.models import (
CachedCombinedTicket, CachedTicket, Device, Event, Invoice, InvoiceAddress, CachedCombinedTicket, CachedTicket, Device, Event, Invoice, InvoiceAddress,
Order, OrderFee, OrderPayment, OrderPosition, OrderRefund, Quota, SubEvent, Order, OrderFee, OrderPayment, OrderPosition, OrderRefund, Quota, SubEvent,
TaxRule, TeamAPIToken, generate_secret, TeamAPIToken, generate_secret,
) )
from pretix.base.models.orders import RevokedTicketSecret from pretix.base.models.orders import RevokedTicketSecret
from pretix.base.payment import PaymentException from pretix.base.payment import PaymentException
@@ -65,7 +65,6 @@ with scopes_disabled():
modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte') modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte')
created_since = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='gte') created_since = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='gte')
subevent_after = django_filters.IsoDateTimeFilter(method='subevent_after_qs') subevent_after = django_filters.IsoDateTimeFilter(method='subevent_after_qs')
subevent_before = django_filters.IsoDateTimeFilter(method='subevent_before_qs')
search = django_filters.CharFilter(method='search_qs') search = django_filters.CharFilter(method='search_qs')
class Meta: class Meta:
@@ -85,19 +84,6 @@ with scopes_disabled():
).filter(has_se_after=True) ).filter(has_se_after=True)
return qs return qs
def subevent_before_qs(self, qs, name, value):
qs = qs.annotate(
has_se_before=Exists(
OrderPosition.all.filter(
subevent_id__in=SubEvent.objects.filter(
Q(date_from__lt=value), event=OuterRef(OuterRef('event_id'))
).values_list('id'),
order_id=OuterRef('pk'),
)
)
).filter(has_se_before=True)
return qs
def search_qs(self, qs, name, value): def search_qs(self, qs, name, value):
u = value u = value
if "-" in value: if "-" in value:
@@ -558,15 +544,10 @@ class OrderViewSet(viewsets.ModelViewSet):
) )
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
if 'send_mail' in request.data and 'send_email' not in request.data:
request.data['send_email'] = request.data['send_mail']
serializer = OrderCreateSerializer(data=request.data, context=self.get_serializer_context()) serializer = OrderCreateSerializer(data=request.data, context=self.get_serializer_context())
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
with transaction.atomic(): with transaction.atomic():
try: self.perform_create(serializer)
self.perform_create(serializer)
except TaxRule.SaleNotAllowed:
raise ValidationError(_('One of the selected products is not available in the selected country.'))
send_mail = serializer._send_mail send_mail = serializer._send_mail
order = serializer.instance order = serializer.instance
if not order.pk: if not order.pk:
@@ -951,7 +932,6 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
def get_serializer_context(self): def get_serializer_context(self):
ctx = super().get_serializer_context() ctx = super().get_serializer_context()
ctx['order'] = get_object_or_404(Order, code=self.kwargs['order'], event=self.request.event) ctx['order'] = get_object_or_404(Order, code=self.kwargs['order'], event=self.request.event)
ctx['event'] = self.request.event
return ctx return ctx
def get_queryset(self): def get_queryset(self):
@@ -959,7 +939,6 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
return order.payments.all() return order.payments.all()
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
send_mail = request.data.get('send_email', True)
serializer = OrderPaymentCreateSerializer(data=request.data, context=self.get_serializer_context()) serializer = OrderPaymentCreateSerializer(data=request.data, context=self.get_serializer_context())
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
with transaction.atomic(): with transaction.atomic():
@@ -975,8 +954,7 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
user=self.request.user if self.request.user.is_authenticated else None, user=self.request.user if self.request.user.is_authenticated else None,
auth=self.request.auth, auth=self.request.auth,
count_waitinglist=False, count_waitinglist=False,
force=request.data.get('force', False), force=request.data.get('force', False)
send_mail=send_mail,
) )
except Quota.QuotaExceededException: except Quota.QuotaExceededException:
pass pass

View File

@@ -6,9 +6,7 @@ from django.shortcuts import get_object_or_404
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django_filters.rest_framework import DjangoFilterBackend, FilterSet from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
from rest_framework import ( from rest_framework import filters, mixins, serializers, status, viewsets
filters, mixins, serializers, status, views, viewsets,
)
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.exceptions import MethodNotAllowed, PermissionDenied from rest_framework.exceptions import MethodNotAllowed, PermissionDenied
from rest_framework.mixins import CreateModelMixin, DestroyModelMixin from rest_framework.mixins import CreateModelMixin, DestroyModelMixin
@@ -17,18 +15,15 @@ from rest_framework.viewsets import GenericViewSet
from pretix.api.models import OAuthAccessToken from pretix.api.models import OAuthAccessToken
from pretix.api.serializers.organizer import ( from pretix.api.serializers.organizer import (
DeviceSerializer, GiftCardSerializer, GiftCardTransactionSerializer, DeviceSerializer, GiftCardSerializer, OrganizerSerializer,
OrganizerSerializer, OrganizerSettingsSerializer, SeatingPlanSerializer, SeatingPlanSerializer, TeamAPITokenSerializer, TeamInviteSerializer,
TeamAPITokenSerializer, TeamInviteSerializer, TeamMemberSerializer, TeamMemberSerializer, TeamSerializer,
TeamSerializer,
) )
from pretix.base.models import ( from pretix.base.models import (
Device, GiftCard, GiftCardTransaction, Organizer, SeatingPlan, Team, Device, GiftCard, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite,
TeamAPIToken, TeamInvite, User, User,
) )
from pretix.base.settings import SETTINGS_AFFECTING_CSS
from pretix.helpers.dicts import merge_dicts from pretix.helpers.dicts import merge_dicts
from pretix.presale.style import regenerate_organizer_css
class OrganizerViewSet(viewsets.ReadOnlyModelViewSet): class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
@@ -196,24 +191,6 @@ class GiftCardViewSet(viewsets.ModelViewSet):
raise MethodNotAllowed("Gift cards cannot be deleted.") raise MethodNotAllowed("Gift cards cannot be deleted.")
class GiftCardTransactionViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = GiftCardTransactionSerializer
queryset = GiftCardTransaction.objects.none()
permission = 'can_manage_gift_cards'
write_permission = 'can_manage_gift_cards'
@cached_property
def giftcard(self):
if self.request.GET.get('include_accepted') == 'true':
qs = self.request.organizer.accepted_gift_cards
else:
qs = self.request.organizer.issued_gift_cards.all()
return get_object_or_404(qs, pk=self.kwargs.get('giftcard'))
def get_queryset(self):
return self.giftcard.transactions.select_related('order', 'order__event')
class TeamViewSet(viewsets.ModelViewSet): class TeamViewSet(viewsets.ModelViewSet):
serializer_class = TeamSerializer serializer_class = TeamSerializer
queryset = Team.objects.none() queryset = Team.objects.none()
@@ -419,37 +396,3 @@ class DeviceViewSet(mixins.CreateModelMixin,
data=self.request.data data=self.request.data
) )
return inst return inst
class OrganizerSettingsView(views.APIView):
permission = 'can_change_organizer_settings'
def get(self, request, *args, **kwargs):
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer)
if 'explain' in request.GET:
return Response({
fname: {
'value': s.data[fname],
'label': getattr(field, '_label', fname),
'help_text': getattr(field, '_help_text', None)
} for fname, field in s.fields.items()
})
return Response(s.data)
def patch(self, request, *wargs, **kwargs):
s = OrganizerSettingsSerializer(
instance=request.organizer.settings, data=request.data, partial=True,
organizer=request.organizer
)
s.is_valid(raise_exception=True)
with transaction.atomic():
s.save()
self.request.organizer.log_action(
'pretix.organizer.settings', user=self.request.user, auth=self.request.auth, data={
k: v for k, v in s.validated_data.items()
}
)
if any(p in s.changed_data for p in SETTINGS_AFFECTING_CSS):
regenerate_organizer_css.apply_async(args=(request.organizer.pk,))
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer)
return Response(s.data)

View File

@@ -7,7 +7,7 @@ import requests
from celery.exceptions import MaxRetriesExceededError from celery.exceptions import MaxRetriesExceededError
from django.db.models import Exists, OuterRef, Q from django.db.models import Exists, OuterRef, Q
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _, pgettext_lazy from django.utils.translation import gettext_lazy as _
from django_scopes import scope, scopes_disabled from django_scopes import scope, scopes_disabled
from requests import RequestException from requests import RequestException
@@ -97,67 +97,6 @@ class ParametrizedOrderWebhookEvent(WebhookEvent):
} }
class ParametrizedEventWebhookEvent(WebhookEvent):
def __init__(self, action_type, verbose_name):
self._action_type = action_type
self._verbose_name = verbose_name
super().__init__()
@property
def action_type(self):
return self._action_type
@property
def verbose_name(self):
return self._verbose_name
def build_payload(self, logentry: LogEntry):
if logentry.action_type == 'pretix.event.deleted':
organizer = logentry.content_object
return {
'notification_id': logentry.pk,
'organizer': organizer.slug,
'event': logentry.parsed_data.get('slug'),
'action': logentry.action_type,
}
event = logentry.content_object
if not event:
return None
return {
'notification_id': logentry.pk,
'organizer': event.organizer.slug,
'event': event.slug,
'action': logentry.action_type,
}
class ParametrizedSubEventWebhookEvent(WebhookEvent):
def __init__(self, action_type, verbose_name):
self._action_type = action_type
self._verbose_name = verbose_name
super().__init__()
@property
def action_type(self):
return self._action_type
@property
def verbose_name(self):
return self._verbose_name
def build_payload(self, logentry: LogEntry):
# do not use content_object, this is also called in deletion
return {
'notification_id': logentry.pk,
'organizer': logentry.event.organizer.slug,
'event': logentry.event.slug,
'subevent': logentry.object_id,
'action': logentry.action_type,
}
class ParametrizedOrderPositionWebhookEvent(ParametrizedOrderWebhookEvent): class ParametrizedOrderPositionWebhookEvent(ParametrizedOrderWebhookEvent):
def build_payload(self, logentry: LogEntry): def build_payload(self, logentry: LogEntry):
@@ -230,69 +169,44 @@ def register_default_webhook_events(sender, **kwargs):
'pretix.event.checkin.reverted', 'pretix.event.checkin.reverted',
_('Ticket check-in reverted'), _('Ticket check-in reverted'),
), ),
ParametrizedEventWebhookEvent(
'pretix.event.added',
_('Event created'),
),
ParametrizedEventWebhookEvent(
'pretix.event.changed',
_('Event details changed'),
),
ParametrizedEventWebhookEvent(
'pretix.event.deleted',
_('Event details changed'),
),
ParametrizedSubEventWebhookEvent(
'pretix.subevent.added',
pgettext_lazy('subevent', 'Event series date added'),
),
ParametrizedSubEventWebhookEvent(
'pretix.subevent.changed',
pgettext_lazy('subevent', 'Event series date changed'),
),
ParametrizedSubEventWebhookEvent(
'pretix.subevent.deleted',
pgettext_lazy('subevent', 'Event series date deleted'),
),
) )
@app.task(base=TransactionAwareTask, acks_late=True) @app.task(base=TransactionAwareTask, acks_late=True)
def notify_webhooks(logentry_ids: list): def notify_webhooks(logentry_id: int):
if not isinstance(logentry_ids, list): logentry = LogEntry.all.select_related('event', 'event__organizer').get(id=logentry_id)
logentry_ids = [logentry_ids]
qs = LogEntry.all.select_related('event', 'event__organizer').filter(id__in=logentry_ids)
_org, _at, webhooks = None, None, None
for logentry in qs:
if not logentry.organizer:
break # We need to know the organizer
notification_type = logentry.webhook_type if not logentry.organizer:
return # We need to know the organizer
if not notification_type: types = get_all_webhook_events()
break # Ignore, no webhooks for this event type notification_type = None
typepath = logentry.action_type
while not notification_type and '.' in typepath:
notification_type = types.get(typepath + ('.*' if typepath != logentry.action_type else ''))
typepath = typepath.rsplit('.', 1)[0]
if _org != logentry.organizer or _at != logentry.action_type or webhooks is None: if not notification_type:
_org = logentry.organizer return # Ignore, no webhooks for this event type
_at = logentry.action_type
# All webhooks that registered for this notification # All webhooks that registered for this notification
event_listener = WebHookEventListener.objects.filter( event_listener = WebHookEventListener.objects.filter(
webhook=OuterRef('pk'), webhook=OuterRef('pk'),
action_type=notification_type.action_type action_type=notification_type.action_type
) )
webhooks = WebHook.objects.annotate(has_el=Exists(event_listener)).filter(
organizer=logentry.organizer,
has_el=True,
enabled=True
)
if logentry.event_id:
webhooks = webhooks.filter(
Q(all_events=True) | Q(limit_events__pk=logentry.event_id)
)
for wh in webhooks: webhooks = WebHook.objects.annotate(has_el=Exists(event_listener)).filter(
send_webhook.apply_async(args=(logentry.id, notification_type.action_type, wh.pk)) organizer=logentry.organizer,
has_el=True,
enabled=True
)
if logentry.event_id:
webhooks = webhooks.filter(
Q(all_events=True) | Q(limit_events__pk=logentry.event_id)
)
for wh in webhooks:
send_webhook.apply_async(args=(logentry_id, notification_type.action_type, wh.pk))
@app.task(base=ProfiledTask, bind=True, max_retries=9, acks_late=True) @app.task(base=ProfiledTask, bind=True, max_retries=9, acks_late=True)
@@ -336,7 +250,7 @@ def send_webhook(self, logentry_id: int, action_type: str, webhook_id: int):
webhook.enabled = False webhook.enabled = False
webhook.save() webhook.save()
elif resp.status_code > 299: elif resp.status_code > 299:
raise self.retry(countdown=2 ** (self.request.retries * 2)) # max is 2 ** (8*2) = 65536 seconds = ~18 hours raise self.retry(countdown=2 ** (self.request.retries * 2))
except RequestException as e: except RequestException as e:
WebHookCall.objects.create( WebHookCall.objects.create(
webhook=webhook, webhook=webhook,
@@ -348,6 +262,6 @@ def send_webhook(self, logentry_id: int, action_type: str, webhook_id: int):
payload=json.dumps(payload), payload=json.dumps(payload),
response_body=str(e)[:1024 * 1024] response_body=str(e)[:1024 * 1024]
) )
raise self.retry(countdown=2 ** (self.request.retries * 2)) # max is 2 ** (8*2) = 65536 seconds = ~18 hours raise self.retry(countdown=2 ** (self.request.retries * 2))
except MaxRetriesExceededError: except MaxRetriesExceededError:
pass pass

View File

@@ -73,8 +73,8 @@ banlist = [
"wtf" "wtf"
] ]
banlist_regex = re.compile('(' + '|'.join(banlist) + ')') blacklist_regex = re.compile('(' + '|'.join(banlist) + ')')
def banned(string): def banned(string):
return bool(banlist_regex.search(string.lower())) return bool(blacklist_regex.search(string.lower()))

View File

@@ -4,4 +4,3 @@ from .invoices import * # noqa
from .json import * # noqa from .json import * # noqa
from .mail import * # noqa from .mail import * # noqa
from .orderlist import * # noqa from .orderlist import * # noqa
from .waitinglist import * # noqa

View File

@@ -41,7 +41,7 @@ class MailExporter(BaseExporter):
initial=[Order.STATUS_PENDING, Order.STATUS_PAID], initial=[Order.STATUS_PENDING, Order.STATUS_PAID],
choices=Order.STATUS_CHOICE, choices=Order.STATUS_CHOICE,
widget=forms.CheckboxSelectMultiple, widget=forms.CheckboxSelectMultiple,
required=True required=False
)), )),
] ]
) )

View File

@@ -53,23 +53,9 @@ class OrderListExporter(MultiSheetListExporter):
initial=True, initial=True,
required=False required=False
)), )),
('include_payment_amounts',
forms.BooleanField(
label=_('Include payment amounts'),
initial=False,
required=False
)),
] ]
) )
def _get_all_payment_methods(self, qs):
pps = dict(get_all_payment_providers())
return sorted([(pp, pps[pp]) for pp in set(
OrderPayment.objects.exclude(provider='free').filter(order__event__in=self.events).values_list(
'provider', flat=True
).distinct()
)], key=lambda pp: pp[0])
def _get_all_tax_rates(self, qs): def _get_all_tax_rates(self, qs):
tax_rates = set( tax_rates = set(
a for a a for a
@@ -147,8 +133,8 @@ class OrderListExporter(MultiSheetListExporter):
for k, label, w in name_scheme['fields']: for k, label, w in name_scheme['fields']:
headers.append(label) headers.append(label)
headers += [ headers += [
_('Address'), _('ZIP code'), _('City'), _('Country'), pgettext('address', 'State'), _('Address'), _('ZIP code'), _('City'), _('Country'), pgettext('address', 'State'), _('VAT ID'),
_('Custom address field'), _('VAT ID'), _('Date of last payment'), _('Fees'), _('Order locale') _('Date of last payment'), _('Fees'), _('Order locale')
] ]
for tr in tax_rates: for tr in tax_rates:
@@ -164,10 +150,6 @@ class OrderListExporter(MultiSheetListExporter):
headers.append(_('Comment')) headers.append(_('Comment'))
headers.append(_('Positions')) headers.append(_('Positions'))
headers.append(_('Payment providers')) headers.append(_('Payment providers'))
if form_data.get('include_payment_amounts'):
payment_methods = self._get_all_payment_methods(qs)
for id, vn in payment_methods:
headers.append(_('Paid by {method}').format(method=vn))
yield headers yield headers
@@ -181,23 +163,6 @@ class OrderListExporter(MultiSheetListExporter):
taxsum=Sum('tax_value'), grosssum=Sum('value') taxsum=Sum('tax_value'), grosssum=Sum('value')
) )
} }
if form_data.get('include_payment_amounts'):
payment_sum_cache = {
(o['order__id'], o['provider']): o['grosssum'] for o in
OrderPayment.objects.values('provider', 'order__id').order_by().filter(
state__in=[OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED]
).annotate(
grosssum=Sum('amount')
)
}
refund_sum_cache = {
(o['order__id'], o['provider']): o['grosssum'] for o in
OrderRefund.objects.values('provider', 'order__id').order_by().filter(
state__in=[OrderRefund.REFUND_STATE_DONE, OrderRefund.REFUND_STATE_TRANSIT]
).annotate(
grosssum=Sum('amount')
)
}
sum_cache = { sum_cache = {
(o['order__id'], o['tax_rate']): o for o in (o['order__id'], o['tax_rate']): o for o in
OrderPosition.objects.values('tax_rate', 'order__id').order_by().annotate( OrderPosition.objects.values('tax_rate', 'order__id').order_by().annotate(
@@ -235,11 +200,10 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.country if order.invoice_address.country else order.invoice_address.country if order.invoice_address.country else
order.invoice_address.country_old, order.invoice_address.country_old,
order.invoice_address.state, order.invoice_address.state,
order.invoice_address.custom_field,
order.invoice_address.vat_id, order.invoice_address.vat_id,
] ]
except InvoiceAddress.DoesNotExist: except InvoiceAddress.DoesNotExist:
row += [''] * (9 + (len(name_scheme['fields']) if name_scheme and len(name_scheme['fields']) > 1 else 0)) row += [''] * (8 + (len(name_scheme['fields']) if name_scheme and 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 '',
@@ -270,14 +234,6 @@ class OrderListExporter(MultiSheetListExporter):
str(self.providers.get(p, p)) for p in sorted(set((order.payment_providers or '').split(','))) str(self.providers.get(p, p)) for p in sorted(set((order.payment_providers or '').split(',')))
if p and p != 'free' if p and p != 'free'
])) ]))
if form_data.get('include_payment_amounts'):
payment_methods = self._get_all_payment_methods(qs)
for id, vn in payment_methods:
row.append(
payment_sum_cache.get((order.id, id), Decimal('0.00')) -
refund_sum_cache.get((order.id, id), Decimal('0.00'))
)
yield row yield row
def iterate_fees(self, form_data: dict): def iterate_fees(self, form_data: dict):

View File

@@ -1,165 +0,0 @@
from collections import OrderedDict
import pytz
from django import forms
from django.db.models import F, Q
from django.dispatch import receiver
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from pretix.base.models.waitinglist import WaitingListEntry
from ..exporter import ListExporter
from ..signals import (
register_data_exporters, register_multievent_data_exporters,
)
class WaitingListExporter(ListExporter):
identifier = 'waitinglist'
verbose_name = _('Waiting list')
# map selected status to label and queryset-filter
status_filters = [
(
'',
_('All entries'),
lambda qs: qs
),
(
'awaiting-voucher',
_('Waiting for a voucher'),
lambda qs: qs.filter(voucher__isnull=True)
),
(
'voucher-assigned',
_('Voucher assigned'),
lambda qs: qs.filter(voucher__isnull=False)
),
(
'awaiting-redemption',
_('Waiting for redemption'),
lambda qs: qs.filter(
voucher__isnull=False,
voucher__redeemed__lt=F('voucher__max_usages'),
).filter(Q(voucher__valid_until__isnull=True) | Q(voucher__valid_until__gt=now()))
),
(
'voucher-redeemed',
_('Voucher redeemed'),
lambda qs: qs.filter(
voucher__isnull=False,
voucher__redeemed__gte=F('voucher__max_usages'),
)
),
(
'voucher-expired',
_('Voucher expired'),
lambda qs: qs.filter(
voucher__isnull=False,
voucher__redeemed__lt=F('voucher__max_usages'),
voucher__valid_until__isnull=False,
voucher__valid_until__lte=now()
)
),
]
def iterate_list(self, form_data):
# create dicts for easier access by key, which is passed by form_data[status]
status_labels = {k: v for k, v, c in self.status_filters}
queryset_mutators = {k: c for k, v, c in self.status_filters}
entries = WaitingListEntry.objects.filter(
event__in=self.events,
).select_related(
'item', 'variation', 'voucher', 'subevent'
).order_by('created')
# apply filter to queryset/entries according to status
# if unknown status-filter is given, django will handle the error
status_filter = form_data.get("status", "")
entries = queryset_mutators[status_filter](entries)
headers = [
_('Date'),
_('Email'),
_('Product name'),
_('Variation'),
_('Event slug'),
_('Event name'),
pgettext_lazy('subevents', 'Date'), # Name of subevent
_('Start date'), # Start date of subevent or event
_('End date'), # End date of subevent or event
_('Language'),
_('Priority'),
_('Status'),
_('Voucher code'),
]
yield headers
yield self.ProgressSetTotal(total=len(entries))
for entry in entries:
if entry.voucher:
if entry.voucher.redeemed >= entry.voucher.max_usages:
status_label = status_labels['voucher-redeemed']
elif not entry.voucher.is_active():
status_label = status_labels['voucher-expired']
else:
status_label = status_labels['voucher-assigned']
else:
status_label = status_labels['awaiting-voucher']
# which event should be used to output dates in columns "Start date" and "End date"
event_for_date_columns = entry.subevent if entry.subevent else entry.event
tz = pytz.timezone(entry.event.settings.timezone)
datetime_format = '%Y-%m-%d %H:%M:%S'
row = [
entry.created.astimezone(tz).strftime(datetime_format), # alternative: .isoformat(),
entry.email,
str(entry.item) if entry.item else "",
str(entry.variation) if entry.variation else "",
entry.event.slug,
entry.event.name,
entry.subevent.name if entry.subevent else "",
event_for_date_columns.date_from.astimezone(tz).strftime(datetime_format),
event_for_date_columns.date_to.astimezone(tz).strftime(datetime_format) if event_for_date_columns.date_to else "",
entry.locale,
str(entry.priority),
status_label,
entry.voucher.code if entry.voucher else '',
]
yield row
@property
def additional_form_fields(self):
return OrderedDict(
[
('status',
forms.ChoiceField(
label=_('Status'),
initial=['awaiting-voucher'],
required=False,
choices=[(k, v) for (k, v, c) in self.status_filters]
)),
]
)
def get_filename(self):
if self.is_multievent:
event = self.events.first()
slug = event.organizer.slug if len(self.events) > 1 else event.slug
else:
slug = self.event.slug
return '{}_waitinglist'.format(slug)
@receiver(register_data_exporters, dispatch_uid="exporter_waitinglist")
def register_waitinglist_exporter(sender, **kwargs):
return WaitingListExporter
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_waitinglist")
def register_multievent_i_waitinglist_exporter(sender, **kwargs):
return WaitingListExporter

View File

@@ -13,13 +13,10 @@ from babel import localedata
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.core.validators import MaxValueValidator, MinValueValidator
from django.db.models import QuerySet from django.db.models import QuerySet
from django.forms import Select from django.forms import Select
from django.utils.formats import date_format
from django.utils.html import escape from django.utils.html import escape
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.timezone import get_current_timezone
from django.utils.translation import ( from django.utils.translation import (
get_language, gettext_lazy as _, pgettext_lazy, get_language, gettext_lazy as _, pgettext_lazy,
) )
@@ -37,9 +34,7 @@ from pretix.base.forms.widgets import (
) )
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.models import InvoiceAddress, Question, QuestionOption from pretix.base.models import InvoiceAddress, Question, QuestionOption
from pretix.base.models.tax import ( from pretix.base.models.tax import EU_COUNTRIES, cc_to_vat_prefix
EU_COUNTRIES, cc_to_vat_prefix, is_eu_country,
)
from pretix.base.settings import ( from pretix.base.settings import (
COUNTRIES_WITH_STATE_IN_ADDRESS, PERSON_NAME_SALUTATIONS, COUNTRIES_WITH_STATE_IN_ADDRESS, PERSON_NAME_SALUTATIONS,
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS, PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS,
@@ -237,43 +232,6 @@ class QuestionCheckboxSelectMultiple(forms.CheckboxSelectMultiple):
option_template_name = 'pretixbase/forms/widgets/checkbox_option_with_links.html' option_template_name = 'pretixbase/forms/widgets/checkbox_option_with_links.html'
class MinDateValidator(MinValueValidator):
def __call__(self, value):
try:
return super().__call__(value)
except ValidationError as e:
e.params['limit_value'] = date_format(e.params['limit_value'], 'SHORT_DATE_FORMAT')
raise e
class MinDateTimeValidator(MinValueValidator):
def __call__(self, value):
try:
return super().__call__(value)
except ValidationError as e:
e.params['limit_value'] = date_format(e.params['limit_value'].astimezone(get_current_timezone()), 'SHORT_DATETIME_FORMAT')
raise e
class MaxDateValidator(MaxValueValidator):
def __call__(self, value):
try:
return super().__call__(value)
except ValidationError as e:
e.params['limit_value'] = date_format(e.params['limit_value'], 'SHORT_DATE_FORMAT')
raise e
class MaxDateTimeValidator(MaxValueValidator):
def __call__(self, value):
try:
return super().__call__(value)
except ValidationError as e:
e.params['limit_value'] = date_format(e.params['limit_value'].astimezone(get_current_timezone()), 'SHORT_DATETIME_FORMAT')
raise e
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
@@ -432,10 +390,9 @@ class BaseQuestionsForm(forms.Form):
elif q.type == Question.TYPE_NUMBER: elif q.type == Question.TYPE_NUMBER:
field = forms.DecimalField( field = forms.DecimalField(
label=label, required=required, label=label, required=required,
min_value=q.valid_number_min or Decimal('0.00'),
max_value=q.valid_number_max,
help_text=q.help_text, help_text=q.help_text,
initial=initial.answer if initial else None, initial=initial.answer if initial else None,
min_value=Decimal('0.00'),
) )
elif q.type == Question.TYPE_STRING: elif q.type == Question.TYPE_STRING:
field = forms.CharField( field = forms.CharField(
@@ -494,21 +451,12 @@ class BaseQuestionsForm(forms.Form):
max_size=10 * 1024 * 1024, max_size=10 * 1024 * 1024,
) )
elif q.type == Question.TYPE_DATE: elif q.type == Question.TYPE_DATE:
attrs = {}
if q.valid_date_min:
attrs['data-min'] = q.valid_date_min.isoformat()
if q.valid_date_max:
attrs['data-max'] = q.valid_date_max.isoformat()
field = forms.DateField( field = forms.DateField(
label=label, required=required, label=label, required=required,
help_text=help_text, help_text=help_text,
initial=dateutil.parser.parse(initial.answer).date() if initial and initial.answer else None, initial=dateutil.parser.parse(initial.answer).date() if initial and initial.answer else None,
widget=DatePickerWidget(attrs), widget=DatePickerWidget(),
) )
if q.valid_date_min:
field.validators.append(MinDateValidator(q.valid_date_min))
if q.valid_date_max:
field.validators.append(MaxDateValidator(q.valid_date_max))
elif q.type == Question.TYPE_TIME: elif q.type == Question.TYPE_TIME:
field = forms.TimeField( field = forms.TimeField(
label=label, required=required, label=label, required=required,
@@ -521,16 +469,8 @@ class BaseQuestionsForm(forms.Form):
label=label, required=required, label=label, required=required,
help_text=help_text, help_text=help_text,
initial=dateutil.parser.parse(initial.answer).astimezone(tz) if initial and initial.answer else None, initial=dateutil.parser.parse(initial.answer).astimezone(tz) if initial and initial.answer else None,
widget=SplitDateTimePickerWidget( widget=SplitDateTimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
time_format=get_format_without_seconds('TIME_INPUT_FORMATS'),
min_date=q.valid_datetime_min,
max_date=q.valid_datetime_max
),
) )
if q.valid_datetime_min:
field.validators.append(MinDateTimeValidator(q.valid_datetime_min))
if q.valid_datetime_max:
field.validators.append(MaxDateTimeValidator(q.valid_datetime_max))
elif q.type == Question.TYPE_PHONENUMBER: elif q.type == Question.TYPE_PHONENUMBER:
babel_locale = 'en' babel_locale = 'en'
# Babel, and therefore django-phonenumberfield, do not support our custom locales such das de_Informal # Babel, and therefore django-phonenumberfield, do not support our custom locales such das de_Informal
@@ -708,7 +648,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
self.fields['state'].widget.is_required = True self.fields['state'].widget.is_required = True
# Without JavaScript the VAT ID field is not hidden, so we empty the field if a country outside the EU is selected. # Without JavaScript the VAT ID field is not hidden, so we empty the field if a country outside the EU is selected.
if cc and not is_eu_country(cc) and fprefix + 'vat_id' in self.data: if cc and cc not in EU_COUNTRIES and fprefix + 'vat_id' in self.data:
self.data = self.data.copy() self.data = self.data.copy()
del self.data[fprefix + 'vat_id'] del self.data[fprefix + 'vat_id']
@@ -758,7 +698,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
if not data.get('is_business'): if not data.get('is_business'):
data['company'] = '' data['company'] = ''
data['vat_id'] = '' data['vat_id'] = ''
if data.get('is_business') and not is_eu_country(data.get('country')): if data.get('is_business') and not data.get('country') in EU_COUNTRIES:
data['vat_id'] = '' data['vat_id'] = ''
if self.event.settings.invoice_address_required: if self.event.settings.invoice_address_required:
if data.get('is_business') and not data.get('company'): if data.get('is_business') and not data.get('company'):
@@ -782,7 +722,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
self.cleaned_data['country'] = '' self.cleaned_data['country'] = ''
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 is_eu_country(data.get('country')) 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'):
if data.get('vat_id')[:2] != cc_to_vat_prefix(str(data.get('country'))): if data.get('vat_id')[:2] != cc_to_vat_prefix(str(data.get('country'))):
raise ValidationError(_('Your VAT ID does not match the selected country.')) raise ValidationError(_('Your VAT ID does not match the selected country.'))
try: try:

View File

@@ -1,10 +1,9 @@
import os import os
from datetime import date
from django import forms from django import forms
from django.utils.formats import get_format from django.utils.formats import get_format
from django.utils.functional import lazy from django.utils.functional import lazy
from django.utils.timezone import get_current_timezone, now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@@ -93,7 +92,7 @@ class UploadedFileWidget(forms.ClearableFileInput):
class SplitDateTimePickerWidget(forms.SplitDateTimeWidget): class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
template_name = 'pretixbase/forms/widgets/splitdatetime.html' template_name = 'pretixbase/forms/widgets/splitdatetime.html'
def __init__(self, attrs=None, date_format=None, time_format=None, min_date=None, max_date=None): def __init__(self, attrs=None, date_format=None, time_format=None):
attrs = attrs or {} attrs = attrs or {}
if 'placeholder' in attrs: if 'placeholder' in attrs:
del attrs['placeholder'] del attrs['placeholder']
@@ -107,14 +106,6 @@ class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
time_attrs['class'] += ' timepickerfield' time_attrs['class'] += ' timepickerfield'
date_attrs['autocomplete'] = 'date-picker-do-not-autofill' date_attrs['autocomplete'] = 'date-picker-do-not-autofill'
time_attrs['autocomplete'] = 'time-picker-do-not-autofill' time_attrs['autocomplete'] = 'time-picker-do-not-autofill'
if min_date:
date_attrs['data-min'] = (
min_date if isinstance(min_date, date) else min_date.astimezone(get_current_timezone()).date()
).isoformat()
if max_date:
date_attrs['data-max'] = (
max_date if isinstance(max_date, date) else max_date.astimezone(get_current_timezone()).date()
).isoformat()
def date_placeholder(): def date_placeholder():
df = date_format or get_format('DATE_INPUT_FORMATS')[0] df = date_format or get_format('DATE_INPUT_FORMATS')[0]

View File

@@ -3,8 +3,7 @@ from urllib.parse import urlsplit
import pytz import pytz
from django.conf import settings from django.conf import settings
from django.http import Http404, HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.middleware.common import CommonMiddleware
from django.urls import get_script_prefix from django.urls import get_script_prefix
from django.utils import timezone, translation from django.utils import timezone, translation
from django.utils.cache import patch_vary_headers from django.utils.cache import patch_vary_headers
@@ -253,15 +252,3 @@ class SecurityMiddleware(MiddlewareMixin):
del resp['Content-Security-Policy'] del resp['Content-Security-Policy']
return resp return resp
class CustomCommonMiddleware(CommonMiddleware):
def get_full_path_with_slash(self, request):
"""
Raise an error regardless of DEBUG mode when in POST, PUT, or PATCH.
"""
new_path = super().get_full_path_with_slash(request)
if request.method in ('POST', 'PUT', 'PATCH'):
raise Http404('Please append a / at the end of the URL')
return new_path

View File

@@ -1,20 +0,0 @@
# Generated by Django 3.0.9 on 2020-11-23 15:51
from django.db import migrations
def remove_old_settings(app, schema_editor):
EventSettingsStore = app.get_model('pretixbase', 'Event_SettingsStore')
EventSettingsStore.objects.filter(key__startswith='payment_', key__endswith='__hidden_url').delete()
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0169_checkinlist_gates'),
]
operations = [
migrations.RunPython(remove_old_settings, migrations.RunPython.noop)
]

View File

@@ -1,49 +0,0 @@
# Generated by Django 3.0.11 on 2020-11-26 16:35
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0170_remove_hidden_urls'),
]
operations = [
migrations.AddField(
model_name='question',
name='valid_date_max',
field=models.DateField(blank=True, null=True),
),
migrations.AddField(
model_name='question',
name='valid_date_min',
field=models.DateField(blank=True, null=True),
),
migrations.AddField(
model_name='question',
name='valid_datetime_max',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='question',
name='valid_datetime_min',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='question',
name='valid_number_max',
field=models.DecimalField(decimal_places=6, max_digits=16, null=True),
),
migrations.AddField(
model_name='question',
name='valid_number_min',
field=models.DecimalField(decimal_places=6, max_digits=16, null=True),
),
migrations.AlterField(
model_name='seat',
name='product',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='seats', to='pretixbase.Item'),
),
]

View File

@@ -1,20 +0,0 @@
# Generated by Django 3.0.9 on 2020-12-02 12:37
from django.db import migrations
import pretix.base.models.fields
from pretix.base.channels import get_all_sales_channels
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0171_auto_20201126_1635'),
]
operations = [
migrations.AddField(
model_name='event',
name='sales_channels',
field=pretix.base.models.fields.MultiStringField(default=list(get_all_sales_channels().keys())),
),
]

View File

@@ -49,8 +49,9 @@ class LoggingMixin:
:param user: The user performing the action (optional) :param user: The user performing the action (optional)
""" """
from pretix.api.models import OAuthAccessToken, OAuthApplication from pretix.api.models import OAuthAccessToken, OAuthApplication
from pretix.api.webhooks import notify_webhooks from pretix.api.webhooks import get_all_webhook_events, notify_webhooks
from ..notifications import get_all_notification_types
from ..services.notifications import notify from ..services.notifications import notify
from .devices import Device from .devices import Device
from .event import Event from .event import Event
@@ -92,11 +93,21 @@ class LoggingMixin:
if save: if save:
logentry.save() logentry.save()
if logentry.notification_type: no_types = get_all_notification_types()
notify.apply_async(args=(logentry.pk,)) wh_types = get_all_webhook_events()
if logentry.webhook_type:
notify_webhooks.apply_async(args=(logentry.pk,))
no_type = None
wh_type = None
typepath = logentry.action_type
while (not no_type or not wh_types) and '.' in typepath:
wh_type = wh_type or wh_types.get(typepath + ('.*' if typepath != logentry.action_type else ''))
no_type = no_type or no_types.get(typepath + ('.*' if typepath != logentry.action_type else ''))
typepath = typepath.rsplit('.', 1)[0]
if no_type:
notify.apply_async(args=(logentry.pk,))
if wh_type:
notify_webhooks.apply_async(args=(logentry.pk,))
return logentry return logentry

View File

@@ -20,7 +20,7 @@ class CheckinList(LoggedModel):
include_pending = models.BooleanField(verbose_name=pgettext_lazy('checkin', 'Include pending orders'), include_pending = models.BooleanField(verbose_name=pgettext_lazy('checkin', 'Include pending orders'),
default=False, default=False,
help_text=_('With this option, people will be able to check in even if the ' help_text=_('With this option, people will be able to check in even if the '
'order has not been paid.')) 'order have not been paid.'))
gates = models.ManyToManyField( gates = models.ManyToManyField(
'Gate', verbose_name=_("Gates"), blank=True, 'Gate', verbose_name=_("Gates"), blank=True,
help_text=_("Does not have any effect for the validation of tickets, only for the automatic configuration of " help_text=_("Does not have any effect for the validation of tickets, only for the automatic configuration of "

View File

@@ -222,15 +222,3 @@ class Device(LoggedModel):
return self.organizer.events.all() return self.organizer.events.all()
else: else:
return self.limit_events.all() return self.limit_events.all()
def get_events_with_permission(self, permission, request=None):
"""
Returns a queryset of events the device has a specific permissions to.
:param request: Ignored, for compatibility with User model
:return: Iterable of Events
"""
if permission in self.permission_set():
return self.get_events_with_any_permission()
else:
return self.organizer.events.none()

View File

@@ -23,7 +23,6 @@ from django_scopes import ScopedManager, scopes_disabled
from i18nfield.fields import I18nCharField, I18nTextField from i18nfield.fields import I18nCharField, I18nTextField
from pretix.base.models.base import LoggedModel from pretix.base.models.base import LoggedModel
from pretix.base.models.fields import MultiStringField
from pretix.base.reldate import RelativeDateWrapper from pretix.base.reldate import RelativeDateWrapper
from pretix.base.validators import EventSlugBanlistValidator from pretix.base.validators import EventSlugBanlistValidator
from pretix.helpers.database import GroupConcat from pretix.helpers.database import GroupConcat
@@ -119,49 +118,25 @@ class EventMixin:
def timezone(self): def timezone(self):
return pytz.timezone(self.settings.timezone) return pytz.timezone(self.settings.timezone)
@property
def effective_presale_end(self):
"""
Returns the effective presale end date, taking for subevents into consideration if the presale end
date might have been further limited by the event-level presale end date
"""
if isinstance(self, SubEvent):
presale_ends = [self.presale_end, self.event.presale_end]
return min(filter(lambda x: x is not None, presale_ends)) if any(presale_ends) else None
else:
return self.presale_end
@property @property
def presale_has_ended(self): def presale_has_ended(self):
""" """
Is true, when ``presale_end`` is set and in the past. Is true, when ``presale_end`` is set and in the past.
""" """
if self.effective_presale_end: if self.presale_end:
return now() > self.effective_presale_end return now() > self.presale_end
elif self.date_to: elif self.date_to:
return now() > self.date_to return now() > self.date_to
else: else:
return now().astimezone(self.timezone).date() > self.date_from.astimezone(self.timezone).date() return now().astimezone(self.timezone).date() > self.date_from.astimezone(self.timezone).date()
@property
def effective_presale_start(self):
"""
Returns the effective presale start date, taking for subevents into consideration if the presale start
date might have been further limited by the event-level presale start date
"""
if isinstance(self, SubEvent):
presale_starts = [self.presale_start, self.event.presale_start]
return max(filter(lambda x: x is not None, presale_starts)) if any(presale_starts) else None
else:
return self.presale_start
@property @property
def presale_is_running(self): def presale_is_running(self):
""" """
Is true, when ``presale_end`` is not set or in the future and ``presale_start`` is not Is true, when ``presale_end`` is not set or in the future and ``presale_start`` is not
set or in the past. set or in the past.
""" """
if self.effective_presale_start and now() < self.effective_presale_start: if self.presale_start and now() < self.presale_start:
return False return False
return not self.presale_has_ended return not self.presale_has_ended
@@ -269,34 +244,6 @@ class EventMixin:
return Quota.AVAILABILITY_RESERVED return Quota.AVAILABILITY_RESERVED
return Quota.AVAILABILITY_GONE return Quota.AVAILABILITY_GONE
def free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False):
qs_annotated = self._seats(ignore_voucher=ignore_voucher)
qs = qs_annotated.filter(has_order=False, has_cart=False, has_voucher=False)
if self.settings.seating_minimal_distance > 0:
qs = qs.filter(has_closeby_taken=False)
if not (sales_channel in self.settings.seating_allow_blocked_seats_for_channel or include_blocked):
qs = qs.filter(blocked=False)
return qs
def total_seats(self, ignore_voucher=None):
return self._seats(ignore_voucher=ignore_voucher)
def taken_seats(self, ignore_voucher=None):
return self._seats(ignore_voucher=ignore_voucher).filter(has_order=True)
def blocked_seats(self, ignore_voucher=None):
qs = self._seats(ignore_voucher=ignore_voucher)
q = (
Q(has_cart=True)
| Q(has_voucher=True)
| Q(blocked=True)
)
if self.settings.seating_minimal_distance > 0:
q |= Q(has_closeby_taken=True, has_order=False)
return qs.filter(q)
@settings_hierarkey.add(parent_field='organizer', cache_namespace='event') @settings_hierarkey.add(parent_field='organizer', cache_namespace='event')
class Event(EventMixin, LoggedModel): class Event(EventMixin, LoggedModel):
@@ -332,8 +279,6 @@ class Event(EventMixin, LoggedModel):
:type plugins: str :type plugins: str
:param has_subevents: Enable event series functionality :param has_subevents: Enable event series functionality
:type has_subevents: bool :type has_subevents: bool
:param sales_channels: A list of sales channel identifiers, that this event is available for sale on
:type sales_channels: list
""" """
settings_namespace = 'event' settings_namespace = 'event'
@@ -412,11 +357,7 @@ class Event(EventMixin, LoggedModel):
) )
seating_plan = models.ForeignKey('SeatingPlan', on_delete=models.PROTECT, null=True, blank=True, seating_plan = models.ForeignKey('SeatingPlan', on_delete=models.PROTECT, null=True, blank=True,
related_name='events') related_name='events')
sales_channels = MultiStringField(
verbose_name=_('Restrict to specific sales channels'),
help_text=_('Only sell tickets for this event on the following sales channels.'),
default=['web'],
)
objects = ScopedManager(organizer='organizer') objects = ScopedManager(organizer='organizer')
class Meta: class Meta:
@@ -453,7 +394,7 @@ class Event(EventMixin, LoggedModel):
if img: if img:
return urljoin(build_absolute_uri(self, 'presale:event.index'), img) return urljoin(build_absolute_uri(self, 'presale:event.index'), img)
def _seats(self, ignore_voucher=None): def free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False):
from .seating import Seat from .seating import Seat
qs_annotated = Seat.annotated(self.seats, self.pk, None, qs_annotated = Seat.annotated(self.seats, self.pk, None,
@@ -461,7 +402,13 @@ class Event(EventMixin, LoggedModel):
minimal_distance=self.settings.seating_minimal_distance, minimal_distance=self.settings.seating_minimal_distance,
distance_only_within_row=self.settings.seating_distance_within_row) distance_only_within_row=self.settings.seating_distance_within_row)
return qs_annotated qs = qs_annotated.filter(has_order=False, has_cart=False, has_voucher=False)
if self.settings.seating_minimal_distance > 0:
qs = qs.filter(has_closeby_taken=False)
if not (sales_channel in self.settings.seating_allow_blocked_seats_for_channel or include_blocked):
qs = qs.filter(blocked=False)
return qs
@property @property
def presale_has_ended(self): def presale_has_ended(self):
@@ -560,14 +507,11 @@ class Event(EventMixin, LoggedModel):
def copy_data_from(self, other): def copy_data_from(self, other):
from ..signals import event_copy_data from ..signals import event_copy_data
from . import ( from . import (
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, Question, Item, ItemAddOn, ItemCategory, ItemMetaValue, Question, Quota,
Quota,
) )
self.plugins = other.plugins self.plugins = other.plugins
self.is_public = other.is_public self.is_public = other.is_public
if other.date_admission:
self.date_admission = self.date_from + (other.date_admission - other.date_from)
self.testmode = other.testmode self.testmode = other.testmode
self.save() self.save()
self.log_action('pretix.object.cloned', data={'source': other.slug, 'source_id': other.pk}) self.log_action('pretix.object.cloned', data={'source': other.slug, 'source_id': other.pk})
@@ -629,14 +573,6 @@ class Event(EventMixin, LoggedModel):
ia.addon_category = category_map[ia.addon_category.pk] ia.addon_category = category_map[ia.addon_category.pk]
ia.save() ia.save()
for ia in ItemBundle.objects.filter(base_item__event=other).prefetch_related('base_item', 'bundled_item', 'bundled_variation'):
ia.pk = None
ia.base_item = item_map[ia.base_item.pk]
ia.bundled_item = item_map[ia.bundled_item.pk]
if ia.bundled_variation:
ia.bundled_variation = variation_map[ia.bundled_variation.pk]
ia.save()
for q in Quota.objects.filter(event=other, subevent__isnull=True).prefetch_related('items', 'variations'): for q in Quota.objects.filter(event=other, subevent__isnull=True).prefetch_related('items', 'variations'):
items = list(q.items.all()) items = list(q.items.all())
vars = list(q.variations.all()) vars = list(q.variations.all())
@@ -1153,13 +1089,19 @@ class SubEvent(EventMixin, LoggedModel):
date_format(self.date_from.astimezone(self.timezone), "TIME_FORMAT") if self.settings.show_times else "" date_format(self.date_from.astimezone(self.timezone), "TIME_FORMAT") if self.settings.show_times else ""
).strip() ).strip()
def _seats(self, ignore_voucher=None): def free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False):
from .seating import Seat from .seating import Seat
qs_annotated = Seat.annotated(self.seats, self.event_id, self, qs_annotated = Seat.annotated(self.seats, self.event_id, self,
ignore_voucher_id=ignore_voucher.pk if ignore_voucher else None, ignore_voucher_id=ignore_voucher.pk if ignore_voucher else None,
minimal_distance=self.settings.seating_minimal_distance, minimal_distance=self.settings.seating_minimal_distance,
distance_only_within_row=self.settings.seating_distance_within_row) distance_only_within_row=self.settings.seating_distance_within_row)
return qs_annotated qs = qs_annotated.filter(has_order=False, has_cart=False, has_voucher=False)
if self.settings.seating_minimal_distance > 0:
qs = qs.filter(has_closeby_taken=False)
if not (sales_channel in self.settings.seating_allow_blocked_seats_for_channel or include_blocked):
qs = qs.filter(blocked=False)
return qs
@cached_property @cached_property
def settings(self): def settings(self):

View File

@@ -314,7 +314,7 @@ class Item(LoggedModel):
) )
allow_waitinglist = models.BooleanField( allow_waitinglist = models.BooleanField(
verbose_name=_("Show a waiting list for this ticket"), verbose_name=_("Show a waiting list for this ticket"),
help_text=_("This will only work if waiting lists are enabled for this event."), help_text=_("This will only work of waiting lists are enabled for this event."),
default=True default=True
) )
show_quota_left = models.NullBooleanField( show_quota_left = models.NullBooleanField(
@@ -1084,18 +1084,6 @@ class Question(LoggedModel):
'Question', null=True, blank=True, on_delete=models.SET_NULL, related_name='dependent_questions' 'Question', null=True, blank=True, on_delete=models.SET_NULL, related_name='dependent_questions'
) )
dependency_values = MultiStringField(default=[]) dependency_values = MultiStringField(default=[])
valid_number_min = models.DecimalField(decimal_places=6, max_digits=16, null=True, blank=True,
verbose_name=_('Minimum value'), help_text=_('Currently not supported in our apps'))
valid_number_max = models.DecimalField(decimal_places=6, max_digits=16, null=True, blank=True,
verbose_name=_('Maximum value'), help_text=_('Currently not supported in our apps'))
valid_date_min = models.DateField(null=True, blank=True,
verbose_name=_('Minimum value'), help_text=_('Currently not supported in our apps'))
valid_date_max = models.DateField(null=True, blank=True,
verbose_name=_('Maximum value'), help_text=_('Currently not supported in our apps'))
valid_datetime_min = models.DateTimeField(null=True, blank=True,
verbose_name=_('Minimum value'), help_text=_('Currently not supported in our apps'))
valid_datetime_max = models.DateTimeField(null=True, blank=True,
verbose_name=_('Maximum value'), help_text=_('Currently not supported in our apps'))
objects = ScopedManager(organizer='event__organizer') objects = ScopedManager(organizer='event__organizer')
@@ -1185,24 +1173,14 @@ class Question(LoggedModel):
answer = formats.sanitize_separators(answer) answer = formats.sanitize_separators(answer)
answer = str(answer).strip() answer = str(answer).strip()
try: try:
v = Decimal(answer) return Decimal(answer)
if self.valid_number_min is not None and v < self.valid_number_min:
raise ValidationError(_('The number is to low.'))
if self.valid_number_max is not None and v > self.valid_number_max:
raise ValidationError(_('The number is to high.'))
return v
except DecimalException: except DecimalException:
raise ValidationError(_('Invalid number input.')) raise ValidationError(_('Invalid number input.'))
elif self.type == Question.TYPE_DATE: elif self.type == Question.TYPE_DATE:
if isinstance(answer, date): if isinstance(answer, date):
return answer return answer
try: try:
dt = dateutil.parser.parse(answer).date() return dateutil.parser.parse(answer).date()
if self.valid_date_min is not None and dt < self.valid_date_min:
raise ValidationError(_('Please choose a later date.'))
if self.valid_date_max is not None and dt > self.valid_date_max:
raise ValidationError(_('Please choose an earlier date.'))
return dt
except: except:
raise ValidationError(_('Invalid date input.')) raise ValidationError(_('Invalid date input.'))
elif self.type == Question.TYPE_TIME: elif self.type == Question.TYPE_TIME:
@@ -1219,14 +1197,9 @@ class Question(LoggedModel):
dt = dateutil.parser.parse(answer) dt = dateutil.parser.parse(answer)
if is_naive(dt): if is_naive(dt):
dt = make_aware(dt, pytz.timezone(self.event.settings.timezone)) dt = make_aware(dt, pytz.timezone(self.event.settings.timezone))
return dt
except: except:
raise ValidationError(_('Invalid datetime input.')) raise ValidationError(_('Invalid datetime input.'))
else:
if self.valid_datetime_min is not None and dt < self.valid_datetime_min:
raise ValidationError(_('Please choose a later date.'))
if self.valid_datetime_max is not None and dt > self.valid_datetime_max:
raise ValidationError(_('Please choose an earlier date.'))
return dt
elif self.type == Question.TYPE_COUNTRYCODE and answer: elif self.type == Question.TYPE_COUNTRYCODE and answer:
c = Country(answer.upper()) c = Country(answer.upper())
if c.name: if c.name:

View File

@@ -63,42 +63,14 @@ class LogEntry(models.Model):
return response return response
return self.action_type return self.action_type
@property
def webhook_type(self):
from pretix.api.webhooks import get_all_webhook_events
wh_types = get_all_webhook_events()
wh_type = None
typepath = self.action_type
while not wh_type and '.' in typepath:
wh_type = wh_type or wh_types.get(typepath + ('.*' if typepath != self.action_type else ''))
typepath = typepath.rsplit('.', 1)[0]
return wh_type
@property
def notification_type(self):
from pretix.base.notifications import get_all_notification_types
no_type = None
no_types = get_all_notification_types()
typepath = self.action_type
while not no_type and '.' in typepath:
no_type = no_type or no_types.get(typepath + ('.*' if typepath != self.action_type else ''))
typepath = typepath.rsplit('.', 1)[0]
return no_type
@cached_property @cached_property
def organizer(self): def organizer(self):
from .organizer import Organizer
if self.event: if self.event:
return self.event.organizer return self.event.organizer
elif hasattr(self.content_object, 'event'): elif hasattr(self.content_object, 'event'):
return self.content_object.event.organizer return self.content_object.event.organizer
elif hasattr(self.content_object, 'organizer'): elif hasattr(self.content_object, 'organizer'):
return self.content_object.organizer return self.content_object.organizer
elif isinstance(self.content_object, Organizer):
return self.content_object
return None return None
@cached_property @cached_property
@@ -216,16 +188,3 @@ class LogEntry(models.Model):
def delete(self, using=None, keep_parents=False): def delete(self, using=None, keep_parents=False):
raise TypeError("Logs cannot be deleted.") raise TypeError("Logs cannot be deleted.")
@classmethod
def bulk_postprocess(cls, objects):
from pretix.api.webhooks import notify_webhooks
from ..services.notifications import notify
to_notify = [o.id for o in objects if o.notification_type]
if to_notify:
notify.apply_async(args=(to_notify,))
to_wh = [o.id for o in objects if o.webhook_type]
if to_wh:
notify_webhooks.apply_async(args=(to_wh,))

View File

@@ -639,7 +639,7 @@ class Order(LockModel, LoggedModel):
return return
if iteration > 20: if iteration > 20:
# Safeguard: If we don't find an unused and non-banlisted code within 20 iterations, we increase # Safeguard: If we don't find an unused and non-blacklisted code within 20 iterations, we increase
# the length. # the length.
length += 1 length += 1
iteration = 0 iteration = 0
@@ -1600,10 +1600,6 @@ class OrderPayment(models.Model):
'local_id': r.local_id, 'local_id': r.local_id,
'provider': r.provider, 'provider': r.provider,
}) })
if self.order.pending_sum + r.amount == Decimal('0.00'):
self.refund.done()
return r return r
@@ -1874,7 +1870,7 @@ class OrderFee(models.Model):
self.tax_rule = self.order.event.settings.tax_rate_default self.tax_rule = self.order.event.settings.tax_rate_default
if self.tax_rule: if self.tax_rule:
tax = self.tax_rule.tax(self.value, base_price_is='gross', invoice_address=ia, force_fixed_gross_price=True) tax = self.tax_rule.tax(self.value, base_price_is='gross', invoice_address=ia)
self.tax_rate = tax.rate self.tax_rate = tax.rate
self.tax_value = tax.tax self.tax_value = tax.tax
else: else:
@@ -2026,11 +2022,9 @@ class OrderPosition(AbstractPosition):
except InvoiceAddress.DoesNotExist: except InvoiceAddress.DoesNotExist:
ia = None ia = None
if self.tax_rule: if self.tax_rule:
tax = self.tax_rule.tax(self.price, invoice_address=ia, base_price_is='gross', force_fixed_gross_price=True) tax = self.tax_rule.tax(self.price, invoice_address=ia, base_price_is='gross')
self.tax_rate = tax.rate self.tax_rate = tax.rate
self.tax_value = tax.tax self.tax_value = tax.tax
if tax.gross != self.price:
raise ValueError('Invalid tax calculation')
else: else:
self.tax_value = Decimal('0.00') self.tax_value = Decimal('0.00')
self.tax_rate = Decimal('0.00') self.tax_rate = Decimal('0.00')
@@ -2040,7 +2034,6 @@ class OrderPosition(AbstractPosition):
if self.tax_rate is None: if self.tax_rate is None:
self._calculate_tax() self._calculate_tax()
self.order.touch() self.order.touch()
if not self.pk: if not self.pk:
while not self.secret or OrderPosition.all.filter( while not self.secret or OrderPosition.all.filter(

View File

@@ -357,15 +357,3 @@ class TeamAPIToken(models.Model):
return self.team.organizer.events.all() return self.team.organizer.events.all()
else: else:
return self.team.limit_events.all() return self.team.limit_events.all()
def get_events_with_permission(self, permission, request=None):
"""
Returns a queryset of events the token has a specific permissions to.
:param request: Ignored, for compatibility with User model
:return: Iterable of Events
"""
if getattr(self.team, permission, False):
return self.get_events_with_any_permission()
else:
return self.team.organizer.events.none()

View File

@@ -130,7 +130,7 @@ class Seat(models.Model):
seat_number = models.CharField(max_length=190, blank=True, default="") seat_number = models.CharField(max_length=190, blank=True, default="")
seat_label = models.CharField(max_length=190, null=True) seat_label = models.CharField(max_length=190, null=True)
seat_guid = models.CharField(max_length=190, db_index=True) seat_guid = models.CharField(max_length=190, db_index=True)
product = models.ForeignKey('Item', null=True, blank=True, related_name='seats', on_delete=models.SET_NULL) product = models.ForeignKey('Item', null=True, blank=True, related_name='seats', on_delete=models.CASCADE)
blocked = models.BooleanField(default=False) blocked = models.BooleanField(default=False)
sorting_rank = models.BigIntegerField(default=0) sorting_rank = models.BigIntegerField(default=0)
x = models.FloatField(null=True) x = models.FloatField(null=True)

View File

@@ -4,10 +4,8 @@ from decimal import Decimal
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.utils.formats import localize from django.utils.formats import localize
from django.utils.timezone import get_current_timezone, now from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext_lazy as _, pgettext
from i18nfield.fields import I18nCharField from i18nfield.fields import I18nCharField
from i18nfield.strings import LazyI18nString
from pretix.base.decimal import round_decimal from pretix.base.decimal import round_decimal
from pretix.base.models.base import LoggedModel from pretix.base.models.base import LoggedModel
@@ -87,14 +85,6 @@ EU_CURRENCIES = {
} }
def is_eu_country(cc):
cc = str(cc)
if cc == 'GB':
return now().astimezone(get_current_timezone()).year <= 2020
else:
return cc in EU_COUNTRIES
def cc_to_vat_prefix(country_code): def cc_to_vat_prefix(country_code):
if country_code == 'GR': if country_code == 'GR':
return 'EL' return 'EL'
@@ -137,9 +127,6 @@ class TaxRule(LoggedModel):
class Meta: class Meta:
ordering = ('event', 'rate', 'id') ordering = ('event', 'rate', 'id')
class SaleNotAllowed(Exception):
pass
def allow_delete(self): def allow_delete(self):
from pretix.base.models.orders import OrderFee, OrderPosition from pretix.base.models.orders import OrderFee, OrderPosition
@@ -182,14 +169,12 @@ class TaxRule(LoggedModel):
return Decimal('0.00') return Decimal('0.00')
if self.has_custom_rules: if self.has_custom_rules:
rule = self.get_matching_rule(invoice_address) rule = self.get_matching_rule(invoice_address)
if rule.get('action', 'vat') == 'block':
raise self.SaleNotAllowed()
if rule.get('action', 'vat') == 'vat' and rule.get('rate') is not None: if rule.get('action', 'vat') == 'vat' and rule.get('rate') is not None:
return Decimal(rule.get('rate')) return Decimal(rule.get('rate'))
return Decimal(self.rate) return Decimal(self.rate)
def tax(self, base_price, base_price_is='auto', currency=None, override_tax_rate=None, invoice_address=None, def tax(self, base_price, base_price_is='auto', currency=None, override_tax_rate=None, invoice_address=None,
subtract_from_gross=Decimal('0.00'), gross_price_is_tax_rate: Decimal = None, force_fixed_gross_price=False): subtract_from_gross=Decimal('0.00'), gross_price_is_tax_rate: Decimal = None):
from .event import Event from .event import Event
try: try:
currency = currency or self.event.currency currency = currency or self.event.currency
@@ -201,7 +186,7 @@ class TaxRule(LoggedModel):
rate = override_tax_rate rate = override_tax_rate
elif invoice_address: elif invoice_address:
adjust_rate = self.tax_rate_for(invoice_address) adjust_rate = self.tax_rate_for(invoice_address)
if (adjust_rate == gross_price_is_tax_rate or force_fixed_gross_price) and base_price_is == 'gross': if adjust_rate == gross_price_is_tax_rate and base_price_is == 'gross':
rate = adjust_rate rate = adjust_rate
elif adjust_rate != rate: elif adjust_rate != rate:
normal_price = self.tax(base_price, base_price_is, currency, subtract_from_gross=subtract_from_gross) normal_price = self.tax(base_price, base_price_is, currency, subtract_from_gross=subtract_from_gross)
@@ -256,7 +241,7 @@ class TaxRule(LoggedModel):
rules = self._custom_rules rules = self._custom_rules
if invoice_address: if invoice_address:
for r in rules: for r in rules:
if r['country'] == 'EU' and not is_eu_country(invoice_address.country): if r['country'] == 'EU' and str(invoice_address.country) not in EU_COUNTRIES:
continue continue
if r['country'] not in ('ZZ', 'EU') and r['country'] != str(invoice_address.country): if r['country'] not in ('ZZ', 'EU') and r['country'] != str(invoice_address.country):
continue continue
@@ -269,25 +254,6 @@ class TaxRule(LoggedModel):
return r return r
return {'action': 'vat'} return {'action': 'vat'}
def invoice_text(self, invoice_address):
if self._custom_rules:
rule = self.get_matching_rule(invoice_address)
t = rule.get('invoice_text', {})
if t and any(l for l in t.values()):
return str(LazyI18nString(t))
if self.is_reverse_charge(invoice_address):
if is_eu_country(invoice_address.country):
return pgettext(
"invoice",
"Reverse Charge: According to Article 194, 196 of Council Directive 2006/112/EEC, VAT liability "
"rests with the service recipient."
)
else:
return pgettext(
"invoice",
"VAT liability rests with the service recipient."
)
def is_reverse_charge(self, invoice_address): def is_reverse_charge(self, invoice_address):
if self._custom_rules: if self._custom_rules:
rule = self.get_matching_rule(invoice_address) rule = self.get_matching_rule(invoice_address)
@@ -299,7 +265,7 @@ class TaxRule(LoggedModel):
if not invoice_address or not invoice_address.country: if not invoice_address or not invoice_address.country:
return False return False
if not is_eu_country(invoice_address.country): if str(invoice_address.country) not in EU_COUNTRIES:
return False return False
if invoice_address.country == self.home_country: if invoice_address.country == self.home_country:
@@ -313,8 +279,6 @@ class TaxRule(LoggedModel):
def _tax_applicable(self, invoice_address): def _tax_applicable(self, invoice_address):
if self._custom_rules: if self._custom_rules:
rule = self.get_matching_rule(invoice_address) rule = self.get_matching_rule(invoice_address)
if rule.get('action', 'vat') == 'block':
raise self.SaleNotAllowed()
return rule.get('action', 'vat') == 'vat' return rule.get('action', 'vat') == 'vat'
if not self.eu_reverse_charge: if not self.eu_reverse_charge:
@@ -325,7 +289,7 @@ class TaxRule(LoggedModel):
# No country specified? Always apply VAT! # No country specified? Always apply VAT!
return True return True
if not is_eu_country(invoice_address.country): if str(invoice_address.country) not in EU_COUNTRIES:
# Non-EU country? Never apply VAT! # Non-EU country? Never apply VAT!
return False return False

View File

@@ -513,7 +513,7 @@ class BasePaymentProvider:
return timing and pricing return timing and pricing
def payment_form_render(self, request: HttpRequest, total: Decimal, order: Order=None) -> str: def payment_form_render(self, request: HttpRequest, total: Decimal) -> str:
""" """
When the user selects this provider as their preferred payment method, When the user selects this provider as their preferred payment method,
they will be shown the HTML you return from this method. they will be shown the HTML you return from this method.
@@ -522,15 +522,13 @@ class BasePaymentProvider:
and render the returned form. If your payment method doesn't require and render the returned form. If your payment method doesn't require
the user to fill out form fields, you should just return a paragraph the user to fill out form fields, you should just return a paragraph
of explanatory text. of explanatory text.
:param order: Only set when this is a change to a new payment method for an existing order.
""" """
form = self.payment_form(request) form = self.payment_form(request)
template = get_template('pretixpresale/event/checkout_payment_form_default.html') template = get_template('pretixpresale/event/checkout_payment_form_default.html')
ctx = {'request': request, 'form': form} ctx = {'request': request, 'form': form}
return template.render(ctx) return template.render(ctx)
def checkout_confirm_render(self, request, order: Order=None) -> str: def checkout_confirm_render(self, request) -> str:
""" """
If the user has successfully filled in their payment data, they will be redirected If the user has successfully filled in their payment data, they will be redirected
to a confirmation page which lists all details of their order for a final review. to a confirmation page which lists all details of their order for a final review.
@@ -539,8 +537,6 @@ class BasePaymentProvider:
In most cases, this should include a short summary of the user's input and In most cases, this should include a short summary of the user's input and
a short explanation on how the payment process will continue. a short explanation on how the payment process will continue.
:param order: Only set when this is a change to a new payment method for an existing order.
""" """
raise NotImplementedError() # NOQA raise NotImplementedError() # NOQA

View File

@@ -121,26 +121,6 @@ DEFAULT_VARIABLES = OrderedDict((
'editor_sample': _('John Doe\nSample company\nSesame Street 42\n12345 Any City\nAtlantis'), 'editor_sample': _('John Doe\nSample company\nSesame Street 42\n12345 Any City\nAtlantis'),
'evaluate': lambda op, order, event: op.address_format() 'evaluate': lambda op, order, event: op.address_format()
}), }),
("attendee_street", {
"label": _("Attendee street"),
"editor_sample": 'Sesame Street 42',
"evaluate": lambda op, order, ev: op.street or (op.addon_to.street if op.addon_to else '')
}),
("attendee_zipcode", {
"label": _("Attendee ZIP code"),
"editor_sample": '12345',
"evaluate": lambda op, order, ev: op.zipcode or (op.addon_to.zipcode if op.addon_to else '')
}),
("attendee_city", {
"label": _("Attendee city"),
"editor_sample": 'Any City',
"evaluate": lambda op, order, ev: op.city or (op.addon_to.city if op.addon_to else '')
}),
("attendee_state", {
"label": _("Attendee state"),
"editor_sample": 'Sample State',
"evaluate": lambda op, order, ev: op.state or (op.addon_to.state if op.addon_to else '')
}),
("attendee_country", { ("attendee_country", {
"label": _("Attendee country"), "label": _("Attendee country"),
"editor_sample": 'Atlantis', "editor_sample": 'Atlantis',
@@ -239,31 +219,11 @@ DEFAULT_VARIABLES = OrderedDict((
"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 ''
}), }),
("invoice_street", {
"label": _("Invoice address street"),
"editor_sample": _("Sesame Street 42"),
"evaluate": lambda op, order, ev: order.invoice_address.street if getattr(order, 'invoice_address', None) else ''
}),
("invoice_zipcode", {
"label": _("Invoice address ZIP code"),
"editor_sample": _("12345"),
"evaluate": lambda op, order, ev: order.invoice_address.zipcode if getattr(order, 'invoice_address', None) else ''
}),
("invoice_city", { ("invoice_city", {
"label": _("Invoice address city"), "label": _("Invoice address city"),
"editor_sample": _("Sample city"), "editor_sample": _("Sample city"),
"evaluate": lambda op, order, ev: order.invoice_address.city if getattr(order, 'invoice_address', None) else '' "evaluate": lambda op, order, ev: order.invoice_address.city if getattr(order, 'invoice_address', None) else ''
}), }),
("invoice_state", {
"label": _("Invoice address state"),
"editor_sample": _("Sample State"),
"evaluate": lambda op, order, ev: order.invoice_address.state if getattr(order, 'invoice_address', None) else ''
}),
("invoice_country", {
"label": _("Invoice address country"),
"editor_sample": _("Atlantis"),
"evaluate": lambda op, order, ev: str(getattr(order.invoice_address.country, 'name', '')) if getattr(order, 'invoice_address', None) else ''
}),
("addons", { ("addons", {
"label": _("List of Add-Ons"), "label": _("List of Add-Ons"),
"editor_sample": _("Add-on 1\nAdd-on 2"), "editor_sample": _("Add-on 1\nAdd-on 2"),

View File

@@ -1,5 +1,4 @@
import base64 import base64
import inspect
import struct import struct
from cryptography.hazmat.backends.openssl.backend import Backend from cryptography.hazmat.backends.openssl.backend import Backend
@@ -53,10 +52,10 @@ class BaseTicketSecretGenerator:
return False return False
def generate_secret(self, item: Item, variation: ItemVariation = None, subevent: SubEvent = None, def generate_secret(self, item: Item, variation: ItemVariation = None, subevent: SubEvent = None,
attendee_name: str = None, current_secret: str = None, force_invalidate=False) -> str: current_secret: str = None, force_invalidate=False) -> str:
""" """
Generate a new secret for a ticket with product ``item``, variation ``variation``, subevent ``subevent``, Generate a new secret for a ticket with product ``item``, variation ``variation``, subevent ``subevent``,
attendee name ``attendee_name`` (can be ``None``) and the current secret ``current_secret`` (if any). and the current secret ``current_secret`` (if any).
The result must be a string that should only contain the characters ``A-Za-z0-9+/=``. The result must be a string that should only contain the characters ``A-Za-z0-9+/=``.
@@ -71,11 +70,6 @@ class BaseTicketSecretGenerator:
If ``force_invalidate`` is set to ``False`` and ``item``, ``variation`` and ``subevent`` have a different value If ``force_invalidate`` is set to ``False`` and ``item``, ``variation`` and ``subevent`` have a different value
as when ``current_secret`` was generated, then this method MAY OR MAY NOT return ``current_secret`` unchanged, as when ``current_secret`` was generated, then this method MAY OR MAY NOT return ``current_secret`` unchanged,
depending on the semantics of the method. depending on the semantics of the method.
.. note:: While it is guaranteed that ``generate_secret`` and the revocation list process are called every
time the ``item``, ``variation``, or ``subevent`` parameters change, it is currently **NOT**
guaranteed that this process is triggered if the ``attendee_name`` parameter changes. You should
therefore not rely on this value for more than informational or debugging purposes.
""" """
raise NotImplementedError() raise NotImplementedError()
@@ -86,7 +80,7 @@ class RandomTicketSecretGenerator(BaseTicketSecretGenerator):
use_revocation_list = False use_revocation_list = False
def generate_secret(self, item: Item, variation: ItemVariation = None, subevent: SubEvent = None, def generate_secret(self, item: Item, variation: ItemVariation = None, subevent: SubEvent = None,
attendee_name: str = None, current_secret: str = None, force_invalidate=False): current_secret: str = None, force_invalidate=False):
if current_secret and not force_invalidate: if current_secret and not force_invalidate:
return current_secret return current_secret
return get_random_string( return get_random_string(
@@ -193,17 +187,12 @@ def assign_ticket_secret(event, position, force_invalidate_if_revokation_list_us
gen = event.ticket_secret_generator gen = event.ticket_secret_generator
if gen.use_revocation_list and force_invalidate_if_revokation_list_used: if gen.use_revocation_list and force_invalidate_if_revokation_list_used:
force_invalidate = True force_invalidate = True
kwargs = {}
if 'attendee_name' in inspect.signature(gen.generate_secret).parameters:
kwargs['attendee_name'] = position.attendee_name
secret = gen.generate_secret( secret = gen.generate_secret(
item=position.item, item=position.item,
variation=position.variation, variation=position.variation,
subevent=position.subevent, subevent=position.subevent,
current_secret=position.secret, current_secret=position.secret,
force_invalidate=force_invalidate, force_invalidate=force_invalidate
**kwargs
) )
changed = position.secret != secret changed = position.secret != secret
if position.secret and changed and gen.use_revocation_list: if position.secret and changed and gen.use_revocation_list:

View File

@@ -65,7 +65,7 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email: if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
real_subject = str(subject).format_map(TolerantDict(email_context)) real_subject = str(subject).format_map(TolerantDict(email_context))
email_context = get_email_context(event_or_subevent=p.subevent or order.event, email_context = get_email_context(event_or_subevent=subevent or order.event,
event=order.event, event=order.event,
refund_amount=refund_amount, refund_amount=refund_amount,
position_or_address=p, position_or_address=p,
@@ -82,12 +82,11 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,)) @app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
def cancel_event(self, event: Event, subevent: int, auto_refund: bool, def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_fixed: str,
keep_fee_fixed: str, keep_fee_per_ticket: str, keep_fee_percentage: str, keep_fees: list=None, keep_fee_percentage: str, keep_fees: list=None, manual_refund: bool=False,
manual_refund: bool=False, send: bool=False, send_subject: dict=None, send_message: dict=None, send: bool=False, send_subject: dict=None, send_message: dict=None,
send_waitinglist: bool=False, send_waitinglist_subject: dict={}, send_waitinglist_message: dict={}, send_waitinglist: bool=False, send_waitinglist_subject: dict={}, send_waitinglist_message: dict={},
user: int=None, refund_as_giftcard: bool=False, giftcard_expires=None, giftcard_conditions=None, user: int=None, refund_as_giftcard: bool=False, giftcard_expires=None, giftcard_conditions=None):
subevents_from: str=None, subevents_to: str=None):
send_subject = LazyI18nString(send_subject) send_subject = LazyI18nString(send_subject)
send_message = LazyI18nString(send_message) send_message = LazyI18nString(send_message)
send_waitinglist_subject = LazyI18nString(send_waitinglist_subject) send_waitinglist_subject = LazyI18nString(send_waitinglist_subject)
@@ -103,20 +102,14 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
pcnt__gt=0 pcnt__gt=0
).all() ).all()
if subevent or subevents_from: if subevent:
if subevent: subevent = event.subevents.get(pk=subevent)
subevents = event.subevents.filter(pk=subevent)
subevent = subevents.first()
subevent_ids = {subevent.pk}
else:
subevents = event.subevents.filter(date_from__gte=subevents_from, date_from__lt=subevents_to)
subevent_ids = set(subevents.values_list('id', flat=True))
has_subevent = OrderPosition.objects.filter(order_id=OuterRef('pk')).filter( has_subevent = OrderPosition.objects.filter(order_id=OuterRef('pk')).filter(
subevent__in=subevents subevent=subevent
) )
has_other_subevent = OrderPosition.objects.filter(order_id=OuterRef('pk')).exclude( has_other_subevent = OrderPosition.objects.filter(order_id=OuterRef('pk')).exclude(
subevent__in=subevents subevent=subevent
) )
orders_to_change = orders_to_cancel.annotate( orders_to_change = orders_to_cancel.annotate(
has_subevent=Exists(has_subevent), has_subevent=Exists(has_subevent),
@@ -131,18 +124,15 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
has_subevent=True, has_other_subevent=False has_subevent=True, has_other_subevent=False
) )
for se in subevents: subevent.log_action(
se.log_action( 'pretix.subevent.canceled', user=user,
'pretix.subevent.canceled', user=user, )
) subevent.active = False
se.active = False subevent.save(update_fields=['active'])
se.save(update_fields=['active']) subevent.log_action(
se.log_action( 'pretix.subevent.changed', user=user, data={'active': False, '_source': 'cancel_event'}
'pretix.subevent.changed', user=user, data={'active': False, '_source': 'cancel_event'} )
)
else: else:
subevents = None
subevent_ids = set()
orders_to_change = event.orders.none() orders_to_change = event.orders.none()
event.log_action( event.log_action(
'pretix.event.canceled', user=user, 'pretix.event.canceled', user=user,
@@ -156,9 +146,7 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
) )
failed = 0 failed = 0
total = orders_to_cancel.count() + orders_to_change.count() total = orders_to_cancel.count() + orders_to_change.count()
qs_wl = event.waitinglistentries.filter(voucher__isnull=True).select_related('subevent') qs_wl = event.waitinglistentries.filter(subevent=subevent, voucher__isnull=True)
if subevents:
qs_wl = qs_wl.filter(subevent__in=subevents)
if send_waitinglist: if send_waitinglist:
total += qs_wl.count() total += qs_wl.count()
counter = 0 counter = 0
@@ -182,10 +170,6 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
fee += Decimal(keep_fee_percentage) / Decimal('100.00') * (o.total - fee_sum) fee += Decimal(keep_fee_percentage) / Decimal('100.00') * (o.total - fee_sum)
if keep_fee_fixed: if keep_fee_fixed:
fee += Decimal(keep_fee_fixed) fee += Decimal(keep_fee_fixed)
if keep_fee_per_ticket:
for p in o.positions.all():
if p.addon_to_id is None:
fee += min(p.price, Decimal(keep_fee_per_ticket))
fee = round_decimal(min(fee, o.payment_refund_sum), event.currency) fee = round_decimal(min(fee, o.payment_refund_sum), event.currency)
_cancel_order(o.pk, user, send_mail=False, cancellation_fee=fee, keep_fees=keep_fee_objects) _cancel_order(o.pk, user, send_mail=False, cancellation_fee=fee, keep_fees=keep_fee_objects)
@@ -217,20 +201,16 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
with transaction.atomic(): with transaction.atomic():
o = event.orders.select_for_update().get(pk=o) o = event.orders.select_for_update().get(pk=o)
total = Decimal('0.00') total = Decimal('0.00')
fee = Decimal('0.00')
positions = [] positions = []
ocm = OrderChangeManager(o, user=user, notify=False) ocm = OrderChangeManager(o, user=user, notify=False)
for p in o.positions.all(): for p in o.positions.all():
if p.subevent_id in subevent_ids: if p.subevent == subevent:
total += p.price total += p.price
ocm.cancel(p) ocm.cancel(p)
positions.append(p) positions.append(p)
if keep_fee_per_ticket: fee = Decimal('0.00')
if p.addon_to_id is None:
fee += min(p.price, Decimal(keep_fee_per_ticket))
if keep_fee_fixed: if keep_fee_fixed:
fee += Decimal(keep_fee_fixed) fee += Decimal(keep_fee_fixed)
if keep_fee_percentage: if keep_fee_percentage:
@@ -266,7 +246,7 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
if send_waitinglist: if send_waitinglist:
for wle in qs_wl: for wle in qs_wl:
_send_wle_mail(wle, send_waitinglist_subject, send_waitinglist_message, wle.subevent) _send_wle_mail(wle, send_waitinglist_subject, send_waitinglist_message, subevent)
counter += 1 counter += 1
if not self.request.called_directly and counter % max(10, total // 100) == 0: if not self.request.called_directly and counter % max(10, total // 100) == 0:

View File

@@ -106,7 +106,6 @@ error_messages = {
'seat_unavailable': _('The seat you selected has already been taken. Please select a different seat.'), 'seat_unavailable': _('The seat you selected has already been taken. Please select a different seat.'),
'seat_multiple': _('You can not select the same seat multiple times.'), 'seat_multiple': _('You can not select the same seat multiple times.'),
'gift_card': _("You entered a gift card instead of a voucher. Gift cards can be entered later on when you're asked for your payment details."), 'gift_card': _("You entered a gift card instead of a voucher. Gift cards can be entered later on when you're asked for your payment details."),
'country_blocked': _('One of the selected products is not available in the selected country.'),
} }
@@ -325,8 +324,6 @@ class CartManager:
custom_price_is_net=cp_is_net if cp_is_net is not None else self.event.settings.display_net_prices, custom_price_is_net=cp_is_net if cp_is_net is not None else self.event.settings.display_net_prices,
invoice_address=self.invoice_address, force_custom_price=force_custom_price, bundled_sum=bundled_sum invoice_address=self.invoice_address, force_custom_price=force_custom_price, bundled_sum=bundled_sum
) )
except TaxRule.SaleNotAllowed:
raise CartError(error_messages['country_blocked'])
except ValueError as e: except ValueError as e:
if str(e) == 'price_too_high': if str(e) == 'price_too_high':
raise CartError(error_messages['price_too_high']) raise CartError(error_messages['price_too_high'])
@@ -1066,7 +1063,6 @@ def update_tax_rates(event: Event, cart_id: str, invoice_address: InvoiceAddress
if pos.tax_rate != rate: if pos.tax_rate != rate:
current_net = pos.price - pos.tax_value current_net = pos.price - pos.tax_value
new_gross = pos.item.tax(current_net, base_price_is='net', invoice_address=invoice_address).gross new_gross = pos.item.tax(current_net, base_price_is='net', invoice_address=invoice_address).gross
totaldiff += new_gross - pos.price
pos.price = new_gross pos.price = new_gross
pos.includes_tax = rate != Decimal('0.00') pos.includes_tax = rate != Decimal('0.00')
pos.override_tax_rate = rate pos.override_tax_rate = rate

View File

@@ -1,13 +1,12 @@
from typing import Any, Dict from typing import Any, Dict
from django.conf import settings
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.utils.timezone import override from django.utils.timezone import override
from django.utils.translation import gettext from django.utils.translation import gettext
from pretix.base.i18n import LazyLocaleException, language from pretix.base.i18n import LazyLocaleException, language
from pretix.base.models import ( from pretix.base.models import (
CachedFile, Device, Event, Organizer, TeamAPIToken, User, cachedfile_name, CachedFile, Event, Organizer, User, cachedfile_name,
) )
from pretix.base.services.tasks import ( from pretix.base.services.tasks import (
ProfiledEventTask, ProfiledOrganizerUserTask, ProfiledEventTask, ProfiledOrganizerUserTask,
@@ -49,13 +48,7 @@ def export(self, event: Event, fileid: str, provider: str, form_data: Dict[str,
@app.task(base=ProfiledOrganizerUserTask, throws=(ExportError,), bind=True) @app.task(base=ProfiledOrganizerUserTask, throws=(ExportError,), bind=True)
def multiexport(self, organizer: Organizer, user: User, device: int, token: int, fileid: str, provider: str, form_data: Dict[str, Any]) -> None: def multiexport(self, organizer: Organizer, user: User, fileid: str, provider: str, form_data: Dict[str, Any]) -> None:
if device:
device = Device.objects.get(pk=device)
if token:
device = TeamAPIToken.objects.get(pk=token)
allowed_events = (device or token or user).get_events_with_permission('can_view_orders')
def set_progress(val): def set_progress(val):
if not self.request.called_directly: if not self.request.called_directly:
self.update_state( self.update_state(
@@ -64,22 +57,10 @@ def multiexport(self, organizer: Organizer, user: User, device: int, token: int,
) )
file = CachedFile.objects.get(id=fileid) file = CachedFile.objects.get(id=fileid)
if user: with language(user.locale), override(user.timezone):
locale = user.locale allowed_events = user.get_events_with_permission('can_view_orders')
timezone = user.timezone
else: events = allowed_events.filter(pk__in=form_data.get('events'))
e = allowed_events.first()
if e:
locale = e.settings.locale
timezone = e.settings.timezone
else:
locale = settings.LANGUAGE_CODE
timezone = settings.TIME_ZONE
with language(locale), override(timezone):
if isinstance(form_data['events'][0], str):
events = allowed_events.filter(slug__in=form_data.get('events'), organizer=organizer)
else:
events = allowed_events.filter(pk__in=form_data.get('events'))
responses = register_multievent_data_exporters.send(organizer) responses = register_multievent_data_exporters.send(organizer)
for receiver, response in responses: for receiver, response in responses:

View File

@@ -24,7 +24,7 @@ from pretix.base.i18n import language
from pretix.base.models import ( from pretix.base.models import (
Invoice, InvoiceAddress, InvoiceLine, Order, OrderFee, Invoice, InvoiceAddress, InvoiceLine, Order, OrderFee,
) )
from pretix.base.models.tax import EU_CURRENCIES from pretix.base.models.tax import EU_COUNTRIES, EU_CURRENCIES
from pretix.base.services.tasks import TransactionAwareTask from pretix.base.services.tasks import TransactionAwareTask
from pretix.base.settings import GlobalSettingsObject from pretix.base.settings import GlobalSettingsObject
from pretix.base.signals import invoice_line_text, periodic_task from pretix.base.signals import invoice_line_text, periodic_task
@@ -142,8 +142,6 @@ def build_invoice(invoice: Invoice) -> Invoice:
reverse_charge = False reverse_charge = False
positions.sort(key=lambda p: p.sort_key) positions.sort(key=lambda p: p.sort_key)
tax_texts = []
for i, p in enumerate(positions): for i, p in enumerate(positions):
if not invoice.event.settings.invoice_include_free and p.price == Decimal('0.00') and not p.addon_c: if not invoice.event.settings.invoice_include_free and p.price == Decimal('0.00') and not p.addon_c:
continue continue
@@ -180,10 +178,22 @@ def build_invoice(invoice: Invoice) -> Invoice:
if p.tax_rule and p.tax_rule.is_reverse_charge(ia) and p.price and not p.tax_value: if p.tax_rule and p.tax_rule.is_reverse_charge(ia) and p.price and not p.tax_value:
reverse_charge = True reverse_charge = True
if p.tax_rule: if reverse_charge:
tax_text = p.tax_rule.invoice_text(ia) if invoice.additional_text:
if tax_text and tax_text not in tax_texts: invoice.additional_text += "<br /><br />"
tax_texts.append(tax_text) if str(invoice.invoice_to_country) in EU_COUNTRIES:
invoice.additional_text += pgettext(
"invoice",
"Reverse Charge: According to Article 194, 196 of Council Directive 2006/112/EEC, VAT liability "
"rests with the service recipient."
)
else:
invoice.additional_text += pgettext(
"invoice",
"VAT liability rests with the service recipient."
)
invoice.reverse_charge = True
invoice.save()
offset = len(positions) offset = len(positions)
for i, fee in enumerate(invoice.order.fees.all()): for i, fee in enumerate(invoice.order.fees.all()):
@@ -203,20 +213,6 @@ def build_invoice(invoice: Invoice) -> Invoice:
tax_name=fee.tax_rule.name if fee.tax_rule else '' tax_name=fee.tax_rule.name if fee.tax_rule else ''
) )
if fee.tax_rule and fee.tax_rule.is_reverse_charge(ia) and fee.value and not fee.tax_value:
reverse_charge = True
if fee.tax_rule:
tax_text = fee.tax_rule.invoice_text(ia)
if tax_text and tax_text not in tax_texts:
tax_texts.append(tax_text)
if tax_texts:
invoice.additional_text += "<br /><br />"
invoice.additional_text += "<br />".join(tax_texts)
invoice.reverse_charge = reverse_charge
invoice.save()
return invoice return invoice

View File

@@ -372,7 +372,7 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
backend.send_messages([email]) backend.send_messages([email])
except smtplib.SMTPResponseException as e: except smtplib.SMTPResponseException as e:
if e.smtp_code in (101, 111, 421, 422, 431, 442, 447, 452): if e.smtp_code in (101, 111, 421, 422, 431, 442, 447, 452):
self.retry(max_retries=5, countdown=2 ** (self.request.retries * 3)) # max is 2 ** (4*3) = 4096 seconds = 68 minutes self.retry(max_retries=5, countdown=2 ** (self.request.retries * 2))
logger.exception('Error sending email') logger.exception('Error sending email')
if order: if order:
@@ -389,7 +389,7 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
raise SendMailException('Failed to send an email to {}.'.format(to)) raise SendMailException('Failed to send an email to {}.'.format(to))
except Exception as e: except Exception as e:
if isinstance(e, (smtplib.SMTPServerDisconnected, smtplib.SMTPConnectError, ssl.SSLError, OSError)): if isinstance(e, (smtplib.SMTPServerDisconnected, smtplib.SMTPConnectError, ssl.SSLError, OSError)):
self.retry(max_retries=5, countdown=2 ** (self.request.retries * 3)) # max is 2 ** (4*3) = 4096 seconds = 68 minutes self.retry(max_retries=5, countdown=2 ** (self.request.retries * 2))
if order: if order:
order.log_action( order.log_action(
'pretix.event.order.email.error', 'pretix.event.order.email.error',

View File

@@ -15,59 +15,55 @@ from pretix.helpers.urls import build_absolute_uri
@app.task(base=TransactionAwareTask, acks_late=True) @app.task(base=TransactionAwareTask, acks_late=True)
@scopes_disabled() @scopes_disabled()
def notify(logentry_ids: list): def notify(logentry_id: int):
if not isinstance(logentry_ids, list): logentry = LogEntry.all.select_related('event', 'event__organizer').get(id=logentry_id)
logentry_ids = [logentry_ids] if not logentry.event:
return # Ignore, we only have event-related notifications right now
types = get_all_notification_types(logentry.event)
qs = LogEntry.all.select_related('event', 'event__organizer').filter(id__in=logentry_ids) notification_type = None
typepath = logentry.action_type
while not notification_type and '.' in typepath:
notification_type = types.get(typepath + ('.*' if typepath != logentry.action_type else ''))
typepath = typepath.rsplit('.', 1)[0]
_event, _at, notify_specific, notify_global = None, None, None, None if not notification_type:
for logentry in qs: return # No suitable plugin
if not logentry.event:
break # Ignore, we only have event-related notifications right now
notification_type = logentry.notification_type # All users that have the permission to get the notification
users = logentry.event.get_users_with_permission(
notification_type.required_permission
).filter(notifications_send=True, is_active=True)
if logentry.user:
users = users.exclude(pk=logentry.user.pk)
if not notification_type: # Get all notification settings, both specific to this event as well as global
break # No suitable plugin notify_specific = {
(ns.user, ns.method): ns.enabled
for ns in NotificationSetting.objects.filter(
event=logentry.event,
action_type=notification_type.action_type,
user__pk__in=users.values_list('pk', flat=True)
)
}
notify_global = {
(ns.user, ns.method): ns.enabled
for ns in NotificationSetting.objects.filter(
event__isnull=True,
action_type=notification_type.action_type,
user__pk__in=users.values_list('pk', flat=True)
)
}
if _event != logentry.event or _at != logentry.action_type or notify_global is None: for um, enabled in notify_specific.items():
_event = logentry.event user, method = um
_at = logentry.action_type if enabled:
# All users that have the permission to get the notification send_notification.apply_async(args=(logentry_id, notification_type.action_type, user.pk, method))
users = logentry.event.get_users_with_permission(
notification_type.required_permission
).filter(notifications_send=True, is_active=True)
if logentry.user:
users = users.exclude(pk=logentry.user.pk)
# Get all notification settings, both specific to this event as well as global for um, enabled in notify_global.items():
notify_specific = { user, method = um
(ns.user, ns.method): ns.enabled if enabled and um not in notify_specific:
for ns in NotificationSetting.objects.filter( send_notification.apply_async(args=(logentry_id, notification_type.action_type, user.pk, method))
event=logentry.event,
action_type=notification_type.action_type,
user__pk__in=users.values_list('pk', flat=True)
)
}
notify_global = {
(ns.user, ns.method): ns.enabled
for ns in NotificationSetting.objects.filter(
event__isnull=True,
action_type=notification_type.action_type,
user__pk__in=users.values_list('pk', flat=True)
)
}
for um, enabled in notify_specific.items():
user, method = um
if enabled:
send_notification.apply_async(args=(logentry.id, notification_type.action_type, user.pk, method))
for um, enabled in notify_global.items():
user, method = um
if enabled and um not in notify_specific:
send_notification.apply_async(args=(logentry.id, notification_type.action_type, user.pk, method))
@app.task(base=ProfiledTask, acks_late=True) @app.task(base=ProfiledTask, acks_late=True)

View File

@@ -89,7 +89,6 @@ error_messages = {
'positions have been removed from your cart.'), 'positions have been removed from your cart.'),
'seat_invalid': _('One of the seats in your order was invalid, we removed the position from your cart.'), 'seat_invalid': _('One of the seats in your order was invalid, we removed the position from your cart.'),
'seat_unavailable': _('One of the seats in your order has been taken in the meantime, we removed the position from your cart.'), 'seat_unavailable': _('One of the seats in your order has been taken in the meantime, we removed the position from your cart.'),
'country_blocked': _('One of the selected products is not available in the selected country.'),
} }
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -616,39 +615,34 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
current_discount = cp.price_before_voucher - cp.price current_discount = cp.price_before_voucher - cp.price
max_discount = max(v_budget[cp.voucher] + current_discount, 0) max_discount = max(v_budget[cp.voucher] + current_discount, 0)
try: if cp.is_bundled:
if cp.is_bundled: try:
try: bundle = cp.addon_to.item.bundles.get(bundled_item=cp.item, bundled_variation=cp.variation)
bundle = cp.addon_to.item.bundles.get(bundled_item=cp.item, bundled_variation=cp.variation) bprice = bundle.designated_price or 0
bprice = bundle.designated_price or 0 except ItemBundle.DoesNotExist:
except ItemBundle.DoesNotExist: bprice = cp.price
bprice = cp.price except ItemBundle.MultipleObjectsReturned:
except ItemBundle.MultipleObjectsReturned: raise OrderError("Invalid product configuration (duplicate bundle)")
raise OrderError("Invalid product configuration (duplicate bundle)") price = get_price(cp.item, cp.variation, cp.voucher, bprice, cp.subevent, custom_price_is_net=False,
price = get_price(cp.item, cp.variation, cp.voucher, bprice, cp.subevent, custom_price_is_net=False, custom_price_is_tax_rate=cp.override_tax_rate,
custom_price_is_tax_rate=cp.override_tax_rate, invoice_address=address, force_custom_price=True, max_discount=max_discount)
invoice_address=address, force_custom_price=True, max_discount=max_discount) pbv = get_price(cp.item, cp.variation, None, bprice, cp.subevent, custom_price_is_net=False,
pbv = get_price(cp.item, cp.variation, None, bprice, cp.subevent, custom_price_is_net=False, custom_price_is_tax_rate=cp.override_tax_rate,
custom_price_is_tax_rate=cp.override_tax_rate, invoice_address=address, force_custom_price=True, max_discount=max_discount)
invoice_address=address, force_custom_price=True, max_discount=max_discount) changed_prices[cp.pk] = bprice
changed_prices[cp.pk] = bprice else:
else: bundled_sum = 0
bundled_sum = 0 if not cp.addon_to_id:
if not cp.addon_to_id: for bundledp in cp.addons.all():
for bundledp in cp.addons.all(): if bundledp.is_bundled:
if bundledp.is_bundled: bundled_sum += changed_prices.get(bundledp.pk, bundledp.price)
bundled_sum += changed_prices.get(bundledp.pk, bundledp.price)
price = get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent, custom_price_is_net=False, price = get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent, custom_price_is_net=False,
addon_to=cp.addon_to, invoice_address=address, bundled_sum=bundled_sum, addon_to=cp.addon_to, invoice_address=address, bundled_sum=bundled_sum,
max_discount=max_discount, custom_price_is_tax_rate=cp.override_tax_rate) max_discount=max_discount, custom_price_is_tax_rate=cp.override_tax_rate)
pbv = get_price(cp.item, cp.variation, None, cp.price, cp.subevent, custom_price_is_net=False, pbv = get_price(cp.item, cp.variation, None, cp.price, cp.subevent, custom_price_is_net=False,
addon_to=cp.addon_to, invoice_address=address, bundled_sum=bundled_sum, addon_to=cp.addon_to, invoice_address=address, bundled_sum=bundled_sum,
max_discount=max_discount, custom_price_is_tax_rate=cp.override_tax_rate) max_discount=max_discount, custom_price_is_tax_rate=cp.override_tax_rate)
except TaxRule.SaleNotAllowed:
err = err or error_messages['country_blocked']
cp.delete()
continue
if max_discount is not None: if max_discount is not None:
v_budget[cp.voucher] = v_budget[cp.voucher] + current_discount - (pbv.gross - price.gross) v_budget[cp.voucher] = v_budget[cp.voucher] + current_discount - (pbv.gross - price.gross)
@@ -1180,7 +1174,6 @@ class OrderChangeManager:
'seat_subevent_mismatch': _('You selected seat "{seat}" for a date that does not match the selected ticket date. Please choose a seat again.'), 'seat_subevent_mismatch': _('You selected seat "{seat}" for a date that does not match the selected ticket date. Please choose a seat again.'),
'seat_required': _('The selected product requires you to select a seat.'), 'seat_required': _('The selected product requires you to select a seat.'),
'seat_forbidden': _('The selected product does not allow to select a seat.'), 'seat_forbidden': _('The selected product does not allow to select a seat.'),
'tax_rule_country_blocked': _('The selected country is blocked by your tax rule.'),
'gift_card_change': _('You cannot change the price of a position that has been used to issue a gift card.'), 'gift_card_change': _('You cannot change the price of a position that has been used to issue a gift card.'),
} }
ItemOperation = namedtuple('ItemOperation', ('position', 'item', 'variation')) ItemOperation = namedtuple('ItemOperation', ('position', 'item', 'variation'))
@@ -1248,11 +1241,8 @@ class OrderChangeManager:
self._operations.append(self.SeatOperation(position, seat)) self._operations.append(self.SeatOperation(position, seat))
def change_subevent(self, position: OrderPosition, subevent: SubEvent): def change_subevent(self, position: OrderPosition, subevent: SubEvent):
try: price = get_price(position.item, position.variation, voucher=position.voucher, subevent=subevent,
price = get_price(position.item, position.variation, voucher=position.voucher, subevent=subevent, invoice_address=self._invoice_address)
invoice_address=self._invoice_address)
except TaxRule.SaleNotAllowed:
raise OrderError(self.error_messages['tax_rule_country_blocked'])
if price is None: # NOQA if price is None: # NOQA
raise OrderError(self.error_messages['product_invalid']) raise OrderError(self.error_messages['product_invalid'])
@@ -1272,11 +1262,8 @@ class OrderChangeManager:
if (not variation and item.has_variations) or (variation and variation.item_id != item.pk): if (not variation and item.has_variations) or (variation and variation.item_id != item.pk):
raise OrderError(self.error_messages['product_without_variation']) raise OrderError(self.error_messages['product_without_variation'])
try: price = get_price(item, variation, voucher=position.voucher, subevent=subevent,
price = get_price(item, variation, voucher=position.voucher, subevent=subevent, invoice_address=self._invoice_address)
invoice_address=self._invoice_address)
except TaxRule.SaleNotAllowed:
raise OrderError(self.error_messages['tax_rule_country_blocked'])
if price is None: # NOQA if price is None: # NOQA
raise OrderError(self.error_messages['product_invalid']) raise OrderError(self.error_messages['product_invalid'])
@@ -1334,10 +1321,7 @@ class OrderChangeManager:
if not pos.price: if not pos.price:
continue continue
try: new_rate = tax_rule.tax_rate_for(ia)
new_rate = tax_rule.tax_rate_for(ia)
except TaxRule.SaleNotAllowed:
raise OrderError(error_messages['tax_rule_country_blocked'])
# We use override_tax_rate to make sure .tax() doesn't get clever and re-adjusts the pricing itself # We use override_tax_rate to make sure .tax() doesn't get clever and re-adjusts the pricing itself
if new_rate != pos.tax_rate: if new_rate != pos.tax_rate:
if keep == 'net': if keep == 'net':
@@ -1390,13 +1374,10 @@ class OrderChangeManager:
except Seat.DoesNotExist: except Seat.DoesNotExist:
raise OrderError(error_messages['seat_invalid']) raise OrderError(error_messages['seat_invalid'])
try: if price is None:
if price is None: price = get_price(item, variation, subevent=subevent, invoice_address=self._invoice_address)
price = get_price(item, variation, subevent=subevent, invoice_address=self._invoice_address) else:
else: price = item.tax(price, base_price_is='gross', invoice_address=self._invoice_address)
price = item.tax(price, base_price_is='gross', invoice_address=self._invoice_address)
except TaxRule.SaleNotAllowed:
raise OrderError(self.error_messages['tax_rule_country_blocked'])
if price is None: if price is None:
raise OrderError(self.error_messages['product_invalid']) raise OrderError(self.error_messages['product_invalid'])
@@ -1971,10 +1952,7 @@ class OrderChangeManager:
self._check_quotas() self._check_quotas()
self._check_seats() self._check_seats()
self._check_complete_cancel() self._check_complete_cancel()
try: self._perform_operations()
self._perform_operations()
except TaxRule.SaleNotAllowed:
raise OrderError(self.error_messages['tax_rule_country_blocked'])
self._recalculate_total_and_payment_fee() self._recalculate_total_and_payment_fee()
self._reissue_invoice() self._reissue_invoice()
self._clear_tickets_cache() self._clear_tickets_cache()

View File

@@ -127,7 +127,6 @@ def order_overview(
order__event=event order__event=event
).annotate( ).annotate(
status=Case( status=Case(
When(order__status='n', order__require_approval=True, then=Value('unapproved')),
When(canceled=True, then=Value('c')), When(canceled=True, then=Value('c')),
default=F('order__status') default=F('order__status')
) )
@@ -136,7 +135,6 @@ def order_overview(
).annotate(cnt=Count('id'), price=Sum('price'), tax_value=Sum('tax_value')).order_by() ).annotate(cnt=Count('id'), price=Sum('price'), tax_value=Sum('tax_value')).order_by()
states = { states = {
'unapproved': 'unapproved',
'canceled': Order.STATUS_CANCELED, 'canceled': Order.STATUS_CANCELED,
'paid': Order.STATUS_PAID, 'paid': Order.STATUS_PAID,
'pending': Order.STATUS_PENDING, 'pending': Order.STATUS_PENDING,
@@ -200,7 +198,6 @@ def order_overview(
order__event=event order__event=event
).annotate( ).annotate(
status=Case( status=Case(
When(order__status='n', order__require_approval=True, then=Value('unapproved')),
When(canceled=True, then=Value('c')), When(canceled=True, then=Value('c')),
default=F('order__status') default=F('order__status')
) )

View File

@@ -96,9 +96,8 @@ class OrganizerUserTask(app.Task):
kwargs['organizer'] = organizer kwargs['organizer'] = organizer
user_id = kwargs['user'] user_id = kwargs['user']
if user_id is not None: user = User.objects.get(pk=user_id)
user = User.objects.get(pk=user_id) kwargs['user'] = user
kwargs['user'] = user
with scope(organizer=organizer): with scope(organizer=organizer):
ret = super().__call__(*args, **kwargs) ret = super().__call__(*args, **kwargs)

View File

@@ -9,9 +9,7 @@ from django import forms
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.files import File from django.core.files import File
from django.core.validators import ( from django.core.validators import MaxValueValidator, MinValueValidator
MaxValueValidator, MinValueValidator, RegexValidator,
)
from django.db.models import Model from django.db.models import Model
from django.utils.translation import ( from django.utils.translation import (
gettext_lazy as _, gettext_noop, pgettext, pgettext_lazy, gettext_lazy as _, gettext_noop, pgettext, pgettext_lazy,
@@ -28,9 +26,7 @@ from pretix.base.reldate import (
RelativeDateField, RelativeDateTimeField, RelativeDateWrapper, RelativeDateField, RelativeDateTimeField, RelativeDateWrapper,
SerializerRelativeDateField, SerializerRelativeDateTimeField, SerializerRelativeDateField, SerializerRelativeDateTimeField,
) )
from pretix.control.forms import ( from pretix.control.forms import MultipleLanguagesWidget, SingleLanguageWidget
FontSelect, MultipleLanguagesWidget, SingleLanguageWidget,
)
from pretix.helpers.countries import CachedCountries from pretix.helpers.countries import CachedCountries
@@ -42,18 +38,6 @@ def country_choice_kwargs():
} }
def primary_font_kwargs():
from pretix.presale.style import get_fonts
choices = [('Open Sans', 'Open Sans')]
choices += [
(a, {"title": a, "data": v}) for a, v in get_fonts().items()
]
return {
'choices': choices,
}
class LazyI18nStringList(UserList): class LazyI18nStringList(UserList):
def __init__(self, init_list=None): def __init__(self, init_list=None):
super().__init__() super().__init__()
@@ -268,6 +252,7 @@ DEFAULTS = {
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Ask for beneficiary"), label=_("Ask for beneficiary"),
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_invoice_address_asked'}), widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_invoice_address_asked'}),
required=False
) )
}, },
'invoice_address_custom_field': { 'invoice_address_custom_field': {
@@ -436,6 +421,7 @@ DEFAULTS = {
widget_kwargs={'attrs': { widget_kwargs={'attrs': {
'rows': 3, 'rows': 3,
}}, }},
required=False,
label=_("Guidance text"), label=_("Guidance text"),
help_text=_("This text will be shown above the payment options. You can explain the choices to the user here, " help_text=_("This text will be shown above the payment options. You can explain the choices to the user here, "
"if you want.") "if you want.")
@@ -455,6 +441,7 @@ DEFAULTS = {
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Set payment term"), label=_("Set payment term"),
widget=forms.RadioSelect, widget=forms.RadioSelect,
required=True,
choices=( choices=(
('days', _("in days")), ('days', _("in days")),
('minutes', _("in minutes")) ('minutes', _("in minutes"))
@@ -501,6 +488,7 @@ DEFAULTS = {
widget=forms.CheckboxInput( widget=forms.CheckboxInput(
attrs={ attrs={
'data-display-dependency': '#id_payment_term_mode_0', 'data-display-dependency': '#id_payment_term_mode_0',
'data-required-if': '#id_payment_term_mode_0'
}, },
), ),
) )
@@ -553,18 +541,6 @@ DEFAULTS = {
"the pool and can be ordered by other people."), "the pool and can be ordered by other people."),
) )
}, },
'payment_pending_hidden': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_('Hide "payment pending" state on customer-facing pages'),
help_text=_("The payment instructions panel will still be shown to the primary customer, but no indication "
"of missing payment will be visible on the ticket pages of attendees who did not buy the ticket "
"themselves.")
)
},
'payment_giftcard__enabled': { 'payment_giftcard__enabled': {
'default': 'True', 'default': 'True',
'type': bool 'type': bool
@@ -1014,16 +990,7 @@ DEFAULTS = {
}, },
'event_list_availability': { 'event_list_availability': {
'default': 'True', 'default': 'True',
'type': bool, 'type': bool
'serializer_class': serializers.BooleanField,
'form_class': forms.BooleanField,
'form_kwargs': dict(
label=_('Show availability in event overviews'),
help_text=_('If checked, the list of events will show if events are sold out. This might '
'make for longer page loading times if you have lots of events and the shown status might be out '
'of date for up to two minutes.'),
required=False
)
}, },
'event_list_type': { 'event_list_type': {
'default': 'list', 'default': 'list',
@@ -1632,106 +1599,26 @@ Your {event} team"""))
'primary_color': { 'primary_color': {
'default': settings.PRETIX_PRIMARY_COLOR, 'default': settings.PRETIX_PRIMARY_COLOR,
'type': str, 'type': str,
'form_class': forms.CharField,
'serializer_class': serializers.CharField,
'serializer_kwargs': dict(
validators=[
RegexValidator(regex='^#[0-9a-fA-F]{6}$',
message=_('Please enter the hexadecimal code of a color, e.g. #990000.')),
],
),
'form_kwargs': dict(
label=_("Primary color"),
validators=[
RegexValidator(regex='^#[0-9a-fA-F]{6}$',
message=_('Please enter the hexadecimal code of a color, e.g. #990000.')),
],
widget=forms.TextInput(attrs={'class': 'colorpickerfield'})
),
}, },
'theme_color_success': { 'theme_color_success': {
'default': '#50A167', 'default': '#50A167',
'type': str, 'type': str
'form_class': forms.CharField,
'serializer_class': serializers.CharField,
'serializer_kwargs': dict(
validators=[
RegexValidator(regex='^#[0-9a-fA-F]{6}$',
message=_('Please enter the hexadecimal code of a color, e.g. #990000.')),
],
),
'form_kwargs': dict(
label=_("Accent color for success"),
help_text=_("We strongly suggest to use a shade of green."),
validators=[
RegexValidator(regex='^#[0-9a-fA-F]{6}$',
message=_('Please enter the hexadecimal code of a color, e.g. #990000.')),
],
widget=forms.TextInput(attrs={'class': 'colorpickerfield'})
),
}, },
'theme_color_danger': { 'theme_color_danger': {
'default': '#D36060', 'default': '#D36060',
'type': str, 'type': str
'form_class': forms.CharField,
'serializer_class': serializers.CharField,
'serializer_kwargs': dict(
validators=[
RegexValidator(regex='^#[0-9a-fA-F]{6}$',
message=_('Please enter the hexadecimal code of a color, e.g. #990000.')),
],
),
'form_kwargs': dict(
label=_("Accent color for errors"),
help_text=_("We strongly suggest to use a shade of red."),
validators=[
RegexValidator(regex='^#[0-9a-fA-F]{6}$',
message=_('Please enter the hexadecimal code of a color, e.g. #990000.')),
],
widget=forms.TextInput(attrs={'class': 'colorpickerfield'})
),
}, },
'theme_color_background': { 'theme_color_background': {
'default': '#FFFFFF', 'default': '#FFFFFF',
'type': str, 'type': str
'form_class': forms.CharField,
'serializer_class': serializers.CharField,
'serializer_kwargs': dict(
validators=[
RegexValidator(regex='^#[0-9a-fA-F]{6}$',
message=_('Please enter the hexadecimal code of a color, e.g. #990000.')),
],
),
'form_kwargs': dict(
label=_("Page background color"),
validators=[
RegexValidator(regex='^#[0-9a-fA-F]{6}$',
message=_('Please enter the hexadecimal code of a color, e.g. #990000.')),
],
widget=forms.TextInput(attrs={'class': 'colorpickerfield no-contrast'})
),
}, },
'theme_round_borders': { 'theme_round_borders': {
'default': 'True', 'default': 'True',
'type': bool, 'type': bool
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Use round edges"),
)
}, },
'primary_font': { 'primary_font': {
'default': 'Open Sans', 'default': 'Open Sans',
'type': str, 'type': str
'form_class': forms.ChoiceField,
'serializer_class': serializers.ChoiceField,
'serializer_kwargs': lambda: dict(**primary_font_kwargs()),
'form_kwargs': lambda: dict(
label=_('Font'),
help_text=_('Only respected by modern browsers.'),
widget=FontSelect,
**primary_font_kwargs()
),
}, },
'presale_css_file': { 'presale_css_file': {
'default': None, 'default': None,
@@ -1767,13 +1654,7 @@ Your {event} team"""))
}, },
'organizer_logo_image_large': { 'organizer_logo_image_large': {
'default': 'False', 'default': 'False',
'type': bool, 'type': bool
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_('Use header image in its full size'),
help_text=_('We recommend to upload a picture at least 1170 pixels wide.'),
)
}, },
'og_image': { 'og_image': {
'default': None, 'default': None,
@@ -1832,19 +1713,6 @@ Your {event} team"""))
"how to obtain a voucher code.") "how to obtain a voucher code.")
) )
}, },
'attendee_data_explanation_text': {
'default': '',
'type': LazyI18nString,
'serializer_class': I18nField,
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Attendee data explanation"),
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown above the questions asked for every admission product. You can use it e.g. to explain "
"why you need information from them.")
)
},
'checkout_email_helptext': { 'checkout_email_helptext': {
'default': LazyI18nString.from_gettext(gettext_noop( 'default': LazyI18nString.from_gettext(gettext_noop(
'Make sure to enter a valid email address. We will send you an order ' 'Make sure to enter a valid email address. We will send you an order '
@@ -1865,26 +1733,11 @@ Your {event} team"""))
}, },
'organizer_info_text': { 'organizer_info_text': {
'default': '', 'default': '',
'type': LazyI18nString, 'type': LazyI18nString
'serializer_class': I18nField,
'form_class': I18nFormField,
'form_kwargs': dict(
label=_('Info text'),
widget=I18nTextarea,
help_text=_('Not displayed anywhere by default, but if you want to, you can use this e.g. in ticket templates.')
)
}, },
'event_team_provisioning': { 'event_team_provisioning': {
'default': 'True', 'default': 'True',
'type': bool, 'type': bool
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_('Allow creating a new team during event creation'),
help_text=_('Users that do not have access to all events under this organizer, must select one of their teams '
'to have access to the created event. This setting allows users to create an event-specified team'
' on-the-fly, even when they do not have \"Can change teams and permissions\" permission.'),
)
}, },
'update_check_ack': { 'update_check_ack': {
'default': 'False', 'default': 'False',
@@ -1926,10 +1779,6 @@ Your {event} team"""))
'default': None, 'default': None,
'type': str 'type': str
}, },
'mapquest_apikey': {
'default': None,
'type': str
},
'leaflet_tiles': { 'leaflet_tiles': {
'default': None, 'default': None,
'type': str 'type': str
@@ -1962,51 +1811,13 @@ Your {event} team"""))
# When adding a new ordering, remember to also define it in the event model # When adding a new ordering, remember to also define it in the event model
) )
}, },
'organizer_link_back': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_('Link back to organizer overview on all event pages'),
)
},
'organizer_homepage_text': {
'default': '',
'type': LazyI18nString,
'serializer_class': I18nField,
'form_class': I18nFormField,
'form_kwargs': dict(
label=_('Homepage text'),
widget=I18nTextarea,
help_text=_('This will be displayed on the organizer homepage.')
)
},
'name_scheme': { 'name_scheme': {
'default': 'full', 'default': 'full',
'type': str 'type': str
}, },
'giftcard_length': { 'giftcard_length': {
'default': settings.ENTROPY['giftcard_secret'], 'default': settings.ENTROPY['giftcard_secret'],
'type': int, 'type': int
'form_class': forms.IntegerField,
'serializer_class': serializers.IntegerField,
'form_kwargs': dict(
label=_('Length of gift card codes'),
help_text=_('The system generates by default {}-character long gift card codes. However, if a different length '
'is required, it can be set here.'.format(settings.ENTROPY['giftcard_secret'])),
)
},
'giftcard_expiry_years': {
'default': None,
'type': int,
'form_class': forms.IntegerField,
'serializer_class': serializers.IntegerField,
'form_kwargs': dict(
label=_('Validity of gift card codes in years'),
help_text=_('If you set a number here, gift cards will by default expire at the end of the year after this '
'many years. If you keep it empty, gift cards do not have an explicit expiry date.'),
)
}, },
'seating_choice': { 'seating_choice': {
'default': 'True', 'default': 'True',
@@ -2042,10 +1853,6 @@ Your {event} team"""))
), ),
} }
} }
SETTINGS_AFFECTING_CSS = {
'primary_color', 'theme_color_success', 'theme_color_danger', 'primary_font',
'theme_color_background', 'theme_round_borders'
}
PERSON_NAME_TITLE_GROUPS = OrderedDict([ PERSON_NAME_TITLE_GROUPS = OrderedDict([
('english_common', (_('Most common English titles'), ( ('english_common', (_('Most common English titles'), (
'Mr', 'Mr',
@@ -2242,31 +2049,7 @@ PERSON_NAME_SCHEMES = OrderedDict([
'title': pgettext_lazy('person_name_sample', 'Dr'), 'title': pgettext_lazy('person_name_sample', 'Dr'),
'given_name': pgettext_lazy('person_name_sample', 'John'), 'given_name': pgettext_lazy('person_name_sample', 'John'),
'family_name': pgettext_lazy('person_name_sample', 'Doe'), 'family_name': pgettext_lazy('person_name_sample', 'Doe'),
'_scheme': 'salutation_title_given_family', '_scheme': 'title_salutation_given_family',
},
}),
('salutation_title_given_family_degree', {
'fields': (
('salutation', pgettext_lazy('person_name', 'Salutation'), 1),
('title', pgettext_lazy('person_name', 'Title'), 1),
('given_name', _('Given name'), 2),
('family_name', _('Family name'), 2),
('degree', pgettext_lazy('person_name', 'Degree (after name)'), 2),
),
'concatenation': lambda d: (
' '.join(
str(p) for p in (d.get(key, '') for key in ["title", "given_name", "family_name"]) if p
) +
str((', ' if d.get('degree') else '')) +
str(d.get('degree', ''))
),
'sample': {
'salutation': pgettext_lazy('person_name_sample', 'Mr'),
'title': pgettext_lazy('person_name_sample', 'Dr'),
'given_name': pgettext_lazy('person_name_sample', 'John'),
'family_name': pgettext_lazy('person_name_sample', 'Doe'),
'degree': pgettext_lazy('person_name_sample', 'MA'),
'_scheme': 'salutation_title_given_family_degree',
}, },
}), }),
]) ])
@@ -2360,8 +2143,7 @@ class SettingsSandbox:
self._event.settings.set(self._convert_key(key), value) self._event.settings.set(self._convert_key(key), value)
def validate_event_settings(event, settings_dict): def validate_settings(event, settings_dict):
from pretix.base.models import Event
from pretix.base.signals import validate_event_settings from pretix.base.signals import validate_event_settings
if 'locales' in settings_dict and settings_dict['locale'] not in settings_dict['locales']: if 'locales' in settings_dict and settings_dict['locale'] not in settings_dict['locales']:
@@ -2392,14 +2174,4 @@ def validate_event_settings(event, settings_dict):
'payment_term_last': _('The last payment date cannot be before the end of presale.') 'payment_term_last': _('The last payment date cannot be before the end of presale.')
}) })
if isinstance(event, Event): validate_event_settings.send(sender=event, settings_dict=settings_dict)
validate_event_settings.send(sender=event, settings_dict=settings_dict)
def validate_organizer_settings(organizer, settings_dict):
# This is not doing anything for the time being.
# But earlier we called validate_event_settings for the organizer, too - and that didn't do anything for
# organizer-settings either.
#
# N.B.: When actually fleshing out this stub, adding it to the OrganizerUpdateForm should be considered.
pass

View File

@@ -4,4 +4,3 @@
<label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}</label>{% endif %}{% if widget.value.is_img %}<br><a href="{{ widget.value.url }}" data-lightbox="{{ widget.value.name }}"><img src="{{ widget.value|thumb:"200x100" }}" /></a>{% endif %}<br> <label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}</label>{% endif %}{% if widget.value.is_img %}<br><a href="{{ widget.value.url }}" data-lightbox="{{ widget.value.name }}"><img src="{{ widget.value|thumb:"200x100" }}" /></a>{% endif %}<br>
{{ widget.input_text }}:{% endif %} {{ widget.input_text }}:{% endif %}
<input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}> <input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>
{% if widget.cachedfile %}<input type="hidden" name="{{ widget.hidden_name }}" value="{{ widget.cachedfile.id }}">{% endif %}

View File

@@ -7,7 +7,6 @@ from django.core.exceptions import ValidationError
from django.core.files import File from django.core.files import File
from django.core.files.uploadedfile import UploadedFile from django.core.files.uploadedfile import UploadedFile
from django.forms.utils import from_current_timezone from django.forms.utils import from_current_timezone
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from ...base.forms import I18nModelForm from ...base.forms import I18nModelForm
@@ -78,8 +77,6 @@ class ClearableBasenameFileInput(forms.ClearableFileInput):
@property @property
def name(self): def name(self):
if hasattr(self.file, 'display_name'):
return self.file.display_name
return self.file.name return self.file.name
@property @property
@@ -87,8 +84,6 @@ class ClearableBasenameFileInput(forms.ClearableFileInput):
return any(self.file.name.lower().endswith(e) for e in ('.jpg', '.jpeg', '.png', '.gif')) return any(self.file.name.lower().endswith(e) for e in ('.jpg', '.jpeg', '.png', '.gif'))
def __str__(self): def __str__(self):
if hasattr(self.file, 'display_name'):
return self.file.display_name
return os.path.basename(self.file.name).split('.', 1)[-1] return os.path.basename(self.file.name).split('.', 1)[-1]
@property @property
@@ -98,48 +93,6 @@ class ClearableBasenameFileInput(forms.ClearableFileInput):
def get_context(self, name, value, attrs): def get_context(self, name, value, attrs):
ctx = super().get_context(name, value, attrs) ctx = super().get_context(name, value, attrs)
ctx['widget']['value'] = self.FakeFile(value) ctx['widget']['value'] = self.FakeFile(value)
ctx['widget']['cachedfile'] = None
return ctx
class CachedFileInput(forms.ClearableFileInput):
template_name = 'pretixbase/forms/widgets/thumbnailed_file_input.html'
class FakeFile(File):
def __init__(self, file):
self.file = file
@property
def name(self):
return self.file.filename
@property
def is_img(self):
return any(self.file.filename.lower().endswith(e) for e in ('.jpg', '.jpeg', '.png', '.gif'))
def __str__(self):
return self.file.filename
@property
def url(self):
return self.file.file.url
def value_from_datadict(self, data, files, name):
from ...base.models import CachedFile
v = super().value_from_datadict(data, files, name)
if v is None and data.get(name + '-cachedfile'): # An explicit "[x] clear" would be False, not None
return CachedFile.objects.filter(id=data[name + '-cachedfile']).first()
return v
def get_context(self, name, value, attrs):
from ...base.models import CachedFile
if isinstance(value, CachedFile):
value = self.FakeFile(value)
ctx = super().get_context(name, value, attrs)
ctx['widget']['value'] = value
ctx['widget']['cachedfile'] = value.file if isinstance(value, self.FakeFile) else None
ctx['widget']['hidden_name'] = name + '-cachedfile'
return ctx return ctx
@@ -176,7 +129,7 @@ class ExtFileField(SizeFileField):
def clean(self, *args, **kwargs): def clean(self, *args, **kwargs):
data = super().clean(*args, **kwargs) data = super().clean(*args, **kwargs)
if isinstance(data, File): if data:
filename = data.name filename = data.name
ext = os.path.splitext(filename)[1] ext = os.path.splitext(filename)[1]
ext = ext.lower() ext = ext.lower()
@@ -185,49 +138,6 @@ class ExtFileField(SizeFileField):
return data return data
class CachedFileField(ExtFileField):
widget = CachedFileInput
def to_python(self, data):
from ...base.models import CachedFile
if isinstance(data, CachedFile):
return data
return super().to_python(data)
def bound_data(self, data, initial):
from ...base.models import CachedFile
if isinstance(data, File):
cf = CachedFile.objects.create(
expires=now() + datetime.timedelta(days=1),
date=now(),
filename=data.name,
type=data.content_type,
)
cf.file.save(data.name, data.file)
cf.save()
return cf
return super().bound_data(data, initial)
def clean(self, *args, **kwargs):
from ...base.models import CachedFile
data = super().clean(*args, **kwargs)
if isinstance(data, File):
cf = CachedFile.objects.create(
expires=now() + datetime.timedelta(days=1),
date=now(),
filename=data.name,
type=data.content_type,
)
cf.file.save(data.name, data.file)
cf.save()
return cf
return data
class SlugWidget(forms.TextInput): class SlugWidget(forms.TextInput):
template_name = 'pretixcontrol/slug_widget.html' template_name = 'pretixcontrol/slug_widget.html'
prefix = '' prefix = ''

View File

@@ -3,14 +3,15 @@ from urllib.parse import urlencode, urlparse
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import validate_email from django.core.validators import RegexValidator, validate_email
from django.db.models import Q from django.db.models import Q
from django.forms import CheckboxSelectMultiple, formset_factory from django.forms import formset_factory
from django.urls import reverse from django.urls import reverse
from django.utils.html import escape from django.utils.html import escape
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.timezone import get_current_timezone_name from django.utils.timezone import get_current_timezone_name
from django.utils.translation import gettext, gettext_lazy as _, pgettext_lazy from django.utils.translation import gettext, gettext_lazy as _, pgettext_lazy
from django_countries import Countries
from django_countries.fields import LazyTypedChoiceField from django_countries.fields import LazyTypedChoiceField
from i18nfield.forms import ( from i18nfield.forms import (
I18nForm, I18nFormField, I18nFormSetMixin, I18nTextarea, I18nTextInput, I18nForm, I18nFormField, I18nFormSetMixin, I18nTextarea, I18nTextInput,
@@ -19,21 +20,18 @@ from pytz import common_timezones, timezone
from pretix.base.channels import get_all_sales_channels from pretix.base.channels import get_all_sales_channels
from pretix.base.email import get_available_placeholders from pretix.base.email import get_available_placeholders
from pretix.base.forms import ( from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
I18nModelForm, PlaceholderValidator, SettingsForm,
)
from pretix.base.models import Event, Organizer, TaxRule, Team from pretix.base.models import Event, Organizer, TaxRule, Team
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 ( from pretix.base.settings import (
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS, validate_event_settings, PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS, validate_settings,
) )
from pretix.control.forms import ( from pretix.control.forms import (
ExtFileField, MultipleLanguagesWidget, SlugWidget, SplitDateTimeField, ExtFileField, FontSelect, MultipleLanguagesWidget, SlugWidget,
SplitDateTimePickerWidget, SplitDateTimeField, SplitDateTimePickerWidget,
) )
from pretix.control.forms.widgets import Select2 from pretix.control.forms.widgets import Select2
from pretix.helpers.countries import CachedCountries
from pretix.multidomain.models import KnownDomain from pretix.multidomain.models import KnownDomain
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.plugins.banktransfer.payment import BankTransfer from pretix.plugins.banktransfer.payment import BankTransfer
@@ -313,16 +311,6 @@ class EventUpdateForm(I18nModelForm):
required=False, required=False,
help_text=_('You need to configure the custom domain in the webserver beforehand.') help_text=_('You need to configure the custom domain in the webserver beforehand.')
) )
self.fields['sales_channels'] = forms.MultipleChoiceField(
label=self.fields['sales_channels'].label,
help_text=self.fields['sales_channels'].help_text,
required=self.fields['sales_channels'].required,
initial=self.fields['sales_channels'].initial,
choices=(
(c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
),
widget=forms.CheckboxSelectMultiple
)
def clean_domain(self): def clean_domain(self):
d = self.cleaned_data['domain'] d = self.cleaned_data['domain']
@@ -379,7 +367,6 @@ class EventUpdateForm(I18nModelForm):
'location', 'location',
'geo_lat', 'geo_lat',
'geo_lon', 'geo_lon',
'sales_channels'
] ]
field_classes = { field_classes = {
'date_from': SplitDateTimeField, 'date_from': SplitDateTimeField,
@@ -394,7 +381,6 @@ class EventUpdateForm(I18nModelForm):
'date_admission': SplitDateTimePickerWidget(attrs={'data-date-default': '#id_date_from_0'}), 'date_admission': SplitDateTimePickerWidget(attrs={'data-date-default': '#id_date_from_0'}),
'presale_start': SplitDateTimePickerWidget(), 'presale_start': SplitDateTimePickerWidget(),
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_presale_start_0'}), 'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_presale_start_0'}),
'sales_channels': CheckboxSelectMultiple()
} }
@@ -445,6 +431,57 @@ class EventSettingsForm(SettingsForm):
'WhatsApp and Reddit only show a square preview, so we recommend to make sure it still looks good ' 'WhatsApp and Reddit only show a square preview, so we recommend to make sure it still looks good '
'only the center square is shown. If you do not fill this, we will use the logo given above.') 'only the center square is shown. If you do not fill this, we will use the logo given above.')
) )
primary_color = forms.CharField(
label=_("Primary color"),
required=False,
validators=[
RegexValidator(regex='^#[0-9a-fA-F]{6}$',
message=_('Please enter the hexadecimal code of a color, e.g. #990000.')),
],
widget=forms.TextInput(attrs={'class': 'colorpickerfield'})
)
theme_color_success = forms.CharField(
label=_("Accent color for success"),
help_text=_("We strongly suggest to use a shade of green."),
required=False,
validators=[
RegexValidator(regex='^#[0-9a-fA-F]{6}$',
message=_('Please enter the hexadecimal code of a color, e.g. #990000.')),
],
widget=forms.TextInput(attrs={'class': 'colorpickerfield'})
)
theme_color_danger = forms.CharField(
label=_("Accent color for errors"),
help_text=_("We strongly suggest to use a dark shade of red."),
required=False,
validators=[
RegexValidator(regex='^#[0-9a-fA-F]{6}$',
message=_('Please enter the hexadecimal code of a color, e.g. #990000.')),
],
widget=forms.TextInput(attrs={'class': 'colorpickerfield'})
)
theme_color_background = forms.CharField(
label=_("Page background color"),
required=False,
validators=[
RegexValidator(regex='^#[0-9a-fA-F]{6}$',
message=_('Please enter the hexadecimal code of a color, e.g. #990000.')),
],
widget=forms.TextInput(attrs={'class': 'colorpickerfield no-contrast'})
)
theme_round_borders = forms.BooleanField(
label=_("Use round edges"),
required=False,
)
primary_font = forms.ChoiceField(
label=_('Font'),
choices=[
('Open Sans', 'Open Sans')
],
widget=FontSelect,
help_text=_('Only respected by modern browsers.')
)
auto_fields = [ auto_fields = [
'imprint_url', 'imprint_url',
@@ -481,25 +518,18 @@ class EventSettingsForm(SettingsForm):
'attendee_company_required', 'attendee_company_required',
'attendee_addresses_asked', 'attendee_addresses_asked',
'attendee_addresses_required', 'attendee_addresses_required',
'attendee_data_explanation_text',
'banner_text', 'banner_text',
'banner_text_bottom', 'banner_text_bottom',
'order_email_asked_twice', 'order_email_asked_twice',
'last_order_modification_date', 'last_order_modification_date',
'checkout_show_copy_answers_button', 'checkout_show_copy_answers_button',
'primary_color',
'theme_color_success',
'theme_color_danger',
'theme_color_background',
'theme_round_borders',
'primary_font',
] ]
def clean(self): def clean(self):
data = super().clean() data = super().clean()
settings_dict = self.event.settings.freeze() settings_dict = self.event.settings.freeze()
settings_dict.update(data) settings_dict.update(data)
validate_event_settings(self.event, data) validate_settings(self.event, data)
return data return data
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@@ -562,7 +592,6 @@ class PaymentSettingsForm(SettingsForm):
'payment_term_last', 'payment_term_last',
'payment_term_expire_automatically', 'payment_term_expire_automatically',
'payment_term_accept_late', 'payment_term_accept_late',
'payment_pending_hidden',
'payment_explanation', 'payment_explanation',
] ]
tax_rate_default = forms.ModelChoiceField( tax_rate_default = forms.ModelChoiceField(
@@ -589,7 +618,7 @@ class PaymentSettingsForm(SettingsForm):
data = super().clean() data = super().clean()
settings_dict = self.obj.settings.freeze() settings_dict = self.obj.settings.freeze()
settings_dict.update(data) settings_dict.update(data)
validate_event_settings(self.obj, data) validate_settings(self.obj, data)
return data return data
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@@ -628,8 +657,6 @@ class ProviderForm(SettingsForm):
enabled = cleaned_data.get(self.settingspref + '_enabled') enabled = cleaned_data.get(self.settingspref + '_enabled')
if not enabled: if not enabled:
return return
if cleaned_data.get(self.settingspref + '_hidden_url', None):
cleaned_data[self.settingspref + '_hidden_url'] = None
for k, v in self.fields.items(): for k, v in self.fields.items():
val = cleaned_data.get(k) val = cleaned_data.get(k)
if v._required and not val: if v._required and not val:
@@ -721,7 +748,7 @@ class InvoiceSettingsForm(SettingsForm):
data = super().clean() data = super().clean()
settings_dict = self.obj.settings.freeze() settings_dict = self.obj.settings.freeze()
settings_dict.update(data) settings_dict.update(data)
validate_event_settings(self.obj, data) validate_settings(self.obj, data)
return data return data
@@ -1092,16 +1119,15 @@ class CommentForm(I18nModelForm):
} }
class CountriesAndEU(CachedCountries): class CountriesAndEU(Countries):
override = { override = {
'ZZ': _('Any country'), 'ZZ': _('Any country'),
'EU': _('European Union') 'EU': _('European Union')
} }
first = ['ZZ', 'EU'] first = ['ZZ', 'EU']
cache_subkey = 'with_any_or_eu'
class TaxRuleLineForm(I18nForm): class TaxRuleLineForm(forms.Form):
country = LazyTypedChoiceField( country = LazyTypedChoiceField(
choices=CountriesAndEU(), choices=CountriesAndEU(),
required=False required=False
@@ -1120,7 +1146,6 @@ class TaxRuleLineForm(I18nForm):
('vat', _('Charge VAT')), ('vat', _('Charge VAT')),
('reverse', _('Reverse charge')), ('reverse', _('Reverse charge')),
('no', _('No VAT')), ('no', _('No VAT')),
('block', _('Sale not allowed')),
], ],
) )
rate = forms.DecimalField( rate = forms.DecimalField(
@@ -1128,26 +1153,11 @@ class TaxRuleLineForm(I18nForm):
max_digits=10, decimal_places=2, max_digits=10, decimal_places=2,
required=False required=False
) )
invoice_text = I18nFormField(
label=_('Text on invoice'),
required=False,
widget=I18nTextInput
)
class I18nBaseFormSet(I18nFormSetMixin, forms.BaseFormSet):
# compatibility shim for django-i18nfield library
def __init__(self, *args, **kwargs):
self.event = kwargs.pop('event', None)
if self.event:
kwargs['locales'] = self.event.settings.get('locales')
super().__init__(*args, **kwargs)
TaxRuleLineFormSet = formset_factory( TaxRuleLineFormSet = formset_factory(
TaxRuleLineForm, formset=I18nBaseFormSet, TaxRuleLineForm,
can_order=True, can_delete=True, extra=0 can_order=False, can_delete=True, extra=0
) )

View File

@@ -1,22 +1,16 @@
from datetime import datetime, time from datetime import datetime, time
from decimal import Decimal
from urllib.parse import urlencode from urllib.parse import urlencode
from django import forms from django import forms
from django.apps import apps from django.apps import apps
from django.conf import settings from django.db.models import Exists, F, OuterRef, Q
from django.db.models import Exists, F, Model, OuterRef, Q, QuerySet
from django.db.models.functions import Coalesce, ExtractWeekDay from django.db.models.functions import Coalesce, ExtractWeekDay
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils.formats import date_format, localize
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.timezone import get_current_timezone, make_aware, now from django.utils.timezone import get_current_timezone, make_aware, now
from django.utils.translation import gettext, gettext_lazy as _, pgettext_lazy from django.utils.translation import gettext_lazy as _, pgettext_lazy
from pretix.base.channels import get_all_sales_channels from pretix.base.forms.widgets import DatePickerWidget
from pretix.base.forms.widgets import (
DatePickerWidget, SplitDateTimePickerWidget,
)
from pretix.base.models import ( from pretix.base.models import (
Checkin, Event, EventMetaProperty, EventMetaValue, Invoice, InvoiceAddress, Checkin, Event, EventMetaProperty, EventMetaValue, Invoice, InvoiceAddress,
Item, Order, OrderPayment, OrderPosition, OrderRefund, Organizer, Question, Item, Order, OrderPayment, OrderPosition, OrderRefund, Organizer, Question,
@@ -25,9 +19,7 @@ from pretix.base.models import (
from pretix.base.signals import register_payment_providers from pretix.base.signals import register_payment_providers
from pretix.control.forms.widgets import Select2 from pretix.control.forms.widgets import Select2
from pretix.control.signals import order_search_filter_q from pretix.control.signals import order_search_filter_q
from pretix.helpers.countries import CachedCountries
from pretix.helpers.database import FixedOrderBy, rolledback_transaction from pretix.helpers.database import FixedOrderBy, rolledback_transaction
from pretix.helpers.dicts import move_to_end
from pretix.helpers.i18n import i18ncomp from pretix.helpers.i18n import i18ncomp
PAYMENT_PROVIDERS = [] PAYMENT_PROVIDERS = []
@@ -91,38 +83,6 @@ class FilterForm(forms.Form):
else: else:
return self.orders[o] return self.orders[o]
def filter_to_strings(self):
string = []
for k, f in self.fields.items():
v = self.cleaned_data.get(k)
if v is None or (isinstance(v, (list, str, QuerySet)) and len(v) == 0):
continue
if k == "saveas":
continue
if isinstance(v, bool):
val = _('Yes') if v else _('No')
elif isinstance(v, QuerySet):
q = ['"' + str(m) + '"' for m in v]
if not q:
continue
val = ' or '.join(q)
elif isinstance(v, Model):
val = '"' + str(v) + '"'
elif isinstance(f, forms.MultipleChoiceField):
valdict = dict(f.choices)
val = ' or '.join([str(valdict.get(m)) for m in v])
elif isinstance(f, forms.ChoiceField):
val = str(dict(f.choices).get(v))
elif isinstance(v, datetime):
val = date_format(v, 'SHORT_DATETIME_FORMAT')
elif isinstance(v, Decimal):
val = localize(v)
else:
val = v
string.append('{}: {}'.format(f.label, val))
return string
class OrderFilterForm(FilterForm): class OrderFilterForm(FilterForm):
query = forms.CharField( query = forms.CharField(
@@ -144,29 +104,20 @@ class OrderFilterForm(FilterForm):
label=_('Order status'), label=_('Order status'),
choices=( choices=(
('', _('All orders')), ('', _('All orders')),
(_('Valid orders'), ( (Order.STATUS_PAID, _('Paid (or canceled with paid fee)')),
(Order.STATUS_PAID, _('Paid (or canceled with paid fee)')), (Order.STATUS_PENDING, _('Pending')),
(Order.STATUS_PENDING, _('Pending')), ('o', _('Pending (overdue)')),
(Order.STATUS_PENDING + Order.STATUS_PAID, _('Pending or paid')), (Order.STATUS_PENDING + Order.STATUS_PAID, _('Pending or paid')),
)), (Order.STATUS_EXPIRED, _('Expired')),
(_('Cancellations'), ( (Order.STATUS_PENDING + Order.STATUS_EXPIRED, _('Pending or expired')),
(Order.STATUS_CANCELED, _('Canceled')), (Order.STATUS_CANCELED, _('Canceled')),
('cp', _('Canceled (or with paid fee)')), ('cp', _('Canceled (or with paid fee)')),
('rc', _('Cancellation requested')), ('pa', _('Approval pending')),
)), ('overpaid', _('Overpaid')),
(_('Payment process'), ( ('underpaid', _('Underpaid')),
(Order.STATUS_EXPIRED, _('Expired')), ('pendingpaid', _('Pending (but fully paid)')),
(Order.STATUS_PENDING + Order.STATUS_EXPIRED, _('Pending or expired')),
('o', _('Pending (overdue)')),
('overpaid', _('Overpaid')),
('underpaid', _('Underpaid')),
('pendingpaid', _('Pending (but fully paid)')),
)),
(_('Approval process'), (
('na', _('Approved, payment pending')),
('pa', _('Approval pending')),
)),
('testmode', _('Test mode')), ('testmode', _('Test mode')),
('rc', _('Cancellation requested')),
), ),
required=False, required=False,
) )
@@ -256,11 +207,6 @@ class OrderFilterForm(FilterForm):
status=Order.STATUS_PENDING, status=Order.STATUS_PENDING,
require_approval=True require_approval=True
) )
elif s == 'na':
qs = qs.filter(
status=Order.STATUS_PENDING,
require_approval=False
)
elif s == 'testmode': elif s == 'testmode':
qs = qs.filter( qs = qs.filter(
testmode=True testmode=True
@@ -391,238 +337,6 @@ class EventOrderFilterForm(OrderFilterForm):
return qs return qs
class FilterNullBooleanSelect(forms.NullBooleanSelect):
def __init__(self, attrs=None):
choices = (
('unknown', _('All')),
('true', _('Yes')),
('false', _('No')),
)
super(forms.NullBooleanSelect, self).__init__(attrs, choices)
class EventOrderExpertFilterForm(EventOrderFilterForm):
subevents_from = forms.SplitDateTimeField(
widget=SplitDateTimePickerWidget(attrs={
}),
label=pgettext_lazy('subevent', 'All dates starting at or after'),
required=False,
)
subevents_to = forms.SplitDateTimeField(
widget=SplitDateTimePickerWidget(attrs={
}),
label=pgettext_lazy('subevent', 'All dates starting before'),
required=False,
)
created_from = forms.SplitDateTimeField(
widget=SplitDateTimePickerWidget(attrs={
}),
label=_('Order placed at or after'),
required=False,
)
created_to = forms.SplitDateTimeField(
widget=SplitDateTimePickerWidget(attrs={
}),
label=_('Order placed before'),
required=False,
)
email = forms.CharField(
required=False,
label=_('E-mail address')
)
comment = forms.CharField(
required=False,
label=_('Comment')
)
locale = forms.ChoiceField(
required=False,
label=_('Locale'),
choices=settings.LANGUAGES
)
email_known_to_work = forms.NullBooleanField(
required=False,
widget=FilterNullBooleanSelect,
label=_('E-mail address verified'),
)
total = forms.DecimalField(
localize=True,
required=False,
label=_('Total amount'),
)
sales_channel = forms.ChoiceField(
label=_('Sales channel'),
required=False,
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
del self.fields['query']
del self.fields['question']
del self.fields['answer']
del self.fields['ordering']
if not self.event.has_subevents:
del self.fields['subevents_from']
del self.fields['subevents_to']
self.fields['sales_channel'].choices = [('', '')] + [
(k, v.verbose_name) for k, v in get_all_sales_channels().items()
]
locale_names = dict(settings.LANGUAGES)
self.fields['locale'].choices = [('', '')] + [(a, locale_names[a]) for a in self.event.settings.locales]
move_to_end(self.fields, 'item')
move_to_end(self.fields, 'provider')
self.fields['invoice_address_company'] = forms.CharField(
required=False,
label=gettext('Invoice address') + ': ' + gettext('Company')
)
self.fields['invoice_address_name'] = forms.CharField(
required=False,
label=gettext('Invoice address') + ': ' + gettext('Name')
)
self.fields['invoice_address_street'] = forms.CharField(
required=False,
label=gettext('Invoice address') + ': ' + gettext('Address')
)
self.fields['invoice_address_zipcode'] = forms.CharField(
required=False,
label=gettext('Invoice address') + ': ' + gettext('ZIP code'),
help_text=_('Exact matches only')
)
self.fields['invoice_address_city'] = forms.CharField(
required=False,
label=gettext('Invoice address') + ': ' + gettext('City'),
help_text=_('Exact matches only')
)
self.fields['invoice_address_country'] = forms.ChoiceField(
required=False,
label=gettext('Invoice address') + ': ' + gettext('Country'),
choices=[('', '')] + list(CachedCountries())
)
self.fields['attendee_name'] = forms.CharField(
required=False,
label=_('Attendee name')
)
self.fields['attendee_email'] = forms.CharField(
required=False,
label=_('Attendee e-mail address')
)
self.fields['attendee_address_company'] = forms.CharField(
required=False,
label=gettext('Attendee address') + ': ' + gettext('Company')
)
self.fields['attendee_address_street'] = forms.CharField(
required=False,
label=gettext('Attendee address') + ': ' + gettext('Address')
)
self.fields['attendee_address_zipcode'] = forms.CharField(
required=False,
label=gettext('Attendee address') + ': ' + gettext('ZIP code'),
help_text=_('Exact matches only')
)
self.fields['attendee_address_city'] = forms.CharField(
required=False,
label=gettext('Attendee address') + ': ' + gettext('City'),
help_text=_('Exact matches only')
)
self.fields['attendee_address_country'] = forms.ChoiceField(
required=False,
label=gettext('Attendee address') + ': ' + gettext('Country'),
choices=[('', '')] + list(CachedCountries())
)
self.fields['ticket_secret'] = forms.CharField(
label=_('Ticket secret'),
required=False
)
for q in self.event.questions.all():
self.fields['question_{}'.format(q.pk)] = forms.CharField(
label=q.question,
required=False,
help_text=_('Exact matches only')
)
def filter_qs(self, qs):
fdata = self.cleaned_data
qs = super().filter_qs(qs)
if fdata.get('subevents_from'):
qs = qs.filter(
all_positions__subevent__date_from__gte=fdata.get('subevents_from'),
all_positions__canceled=False
).distinct()
if fdata.get('subevents_to'):
qs = qs.filter(
all_positions__subevent__date_from__lt=fdata.get('subevents_to'),
all_positions__canceled=False
).distinct()
if fdata.get('email'):
qs = qs.filter(
email__icontains=fdata.get('email')
)
if fdata.get('created_from'):
qs = qs.filter(datetime__gte=fdata.get('created_from'))
if fdata.get('created_to'):
qs = qs.filter(datetime__lte=fdata.get('created_to'))
if fdata.get('comment'):
qs = qs.filter(comment__icontains=fdata.get('comment'))
if fdata.get('sales_channel'):
qs = qs.filter(sales_channel=fdata.get('sales_channel'))
if fdata.get('total'):
qs = qs.filter(total=fdata.get('total'))
if fdata.get('email_known_to_work') is not None:
qs = qs.filter(email_known_to_work=fdata.get('email_known_to_work'))
if fdata.get('locale'):
qs = qs.filter(locale=fdata.get('locale'))
if fdata.get('invoice_address_company'):
qs = qs.filter(invoice_address__company__icontains=fdata.get('invoice_address_company'))
if fdata.get('invoice_address_name'):
qs = qs.filter(invoice_address__name_cached__icontains=fdata.get('invoice_address_name'))
if fdata.get('invoice_address_street'):
qs = qs.filter(invoice_address__street__icontains=fdata.get('invoice_address_street'))
if fdata.get('invoice_address_zipcode'):
qs = qs.filter(invoice_address__zipcode__iexact=fdata.get('invoice_address_zipcode'))
if fdata.get('invoice_address_city'):
qs = qs.filter(invoice_address__city__iexact=fdata.get('invoice_address_city'))
if fdata.get('invoice_address_country'):
qs = qs.filter(invoice_address__country=fdata.get('invoice_address_country'))
if fdata.get('attendee_name'):
qs = qs.filter(
all_positions__attendee_name_cached__icontains=fdata.get('attendee_name')
)
if fdata.get('attendee_address_company'):
qs = qs.filter(
all_positions__company__icontains=fdata.get('attendee_address_company')
).distinct()
if fdata.get('attendee_address_street'):
qs = qs.filter(
all_positions__street__icontains=fdata.get('attendee_address_street')
).distinct()
if fdata.get('attendee_address_city'):
qs = qs.filter(
all_positions__city__iexact=fdata.get('attendee_address_city')
).distinct()
if fdata.get('attendee_address_country'):
qs = qs.filter(
all_positions__country=fdata.get('attendee_address_country')
).distinct()
if fdata.get('ticket_secret'):
qs = qs.filter(
all_positions__secret__icontains=fdata.get('ticket_secret')
).distinct()
for q in self.event.questions.all():
if fdata.get(f'question_{q.pk}'):
answers = QuestionAnswer.objects.filter(
question_id=q.pk,
orderposition__order_id=OuterRef('pk'),
answer__iexact=fdata.get(f'question_{q.pk}')
)
qs = qs.annotate(**{f'q_{q.pk}': Exists(answers)}).filter(**{f'q_{q.pk}': True})
return qs
class OrderSearchFilterForm(OrderFilterForm): class OrderSearchFilterForm(OrderFilterForm):
orders = {'code': 'code', 'email': 'email', 'total': 'total', orders = {'code': 'code', 'email': 'email', 'total': 'total',
'datetime': 'datetime', 'status': 'status', 'datetime': 'datetime', 'status': 'status',
@@ -1113,8 +827,8 @@ class CheckInFilterForm(FilterForm):
'-item': ('-item__name', '-variation__value', '-order__code'), '-item': ('-item__name', '-variation__value', '-order__code'),
'seat': ('seat__sorting_rank', 'seat__guid'), 'seat': ('seat__sorting_rank', 'seat__guid'),
'-seat': ('-seat__sorting_rank', '-seat__guid'), '-seat': ('-seat__sorting_rank', '-seat__guid'),
'date': ('subevent__date_from', 'subevent__id', 'order__code'), 'date': ('subevent__date_from', 'order__code'),
'-date': ('-subevent__date_from', 'subevent__id', '-order__code'), '-date': ('-subevent__date_from', '-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_cached', 'addon_to__attendee_name_cached')}, '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),

View File

@@ -41,10 +41,6 @@ class GlobalSettingsForm(SettingsForm):
required=False, required=False,
label=_("OpenCage API key for geocoding"), label=_("OpenCage API key for geocoding"),
)), )),
('mapquest_apikey', SecretKeySettingsField(
required=False,
label=_("MapQuest API key for geocoding"),
)),
('leaflet_tiles', forms.CharField( ('leaflet_tiles', forms.CharField(
required=False, required=False,
label=_("Leaflet tiles URL pattern"), label=_("Leaflet tiles URL pattern"),

View File

@@ -16,7 +16,6 @@ from i18nfield.forms import I18nFormField, I18nTextarea
from pretix.base.channels import get_all_sales_channels from pretix.base.channels import get_all_sales_channels
from pretix.base.forms import I18nFormSet, I18nModelForm from pretix.base.forms import I18nFormSet, I18nModelForm
from pretix.base.forms.widgets import DatePickerWidget
from pretix.base.models import ( from pretix.base.models import (
Item, ItemCategory, ItemVariation, Question, QuestionOption, Quota, Item, ItemCategory, ItemVariation, Question, QuestionOption, Quota,
) )
@@ -112,26 +111,14 @@ class QuestionForm(I18nModelForm):
'dependency_question', 'dependency_question',
'dependency_values', 'dependency_values',
'print_on_invoice', 'print_on_invoice',
'valid_number_min',
'valid_number_max',
'valid_datetime_min',
'valid_datetime_max',
'valid_date_min',
'valid_date_max',
] ]
widgets = { widgets = {
'valid_datetime_min': SplitDateTimePickerWidget(),
'valid_datetime_max': SplitDateTimePickerWidget(),
'valid_date_min': DatePickerWidget(),
'valid_date_max': DatePickerWidget(),
'items': forms.CheckboxSelectMultiple( 'items': forms.CheckboxSelectMultiple(
attrs={'class': 'scrolling-multiple-choice'} attrs={'class': 'scrolling-multiple-choice'}
), ),
'dependency_values': forms.SelectMultiple, 'dependency_values': forms.SelectMultiple,
} }
field_classes = { field_classes = {
'valid_datetime_min': SplitDateTimeField,
'valid_datetime_max': SplitDateTimeField,
'items': SafeModelMultipleChoiceField, 'items': SafeModelMultipleChoiceField,
'dependency_question': SafeModelChoiceField, 'dependency_question': SafeModelChoiceField,
} }
@@ -239,8 +226,6 @@ class ItemCreateForm(I18nModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.event = kwargs['event'] self.event = kwargs['event']
self.user = kwargs.pop('user') self.user = kwargs.pop('user')
kwargs.setdefault('initial', {})
kwargs['initial'].setdefault('admission', True)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['category'].queryset = self.instance.event.categories.all() self.fields['category'].queryset = self.instance.event.categories.all()

View File

@@ -400,6 +400,7 @@ class OrderPositionChangeForm(forms.Form):
self.fields['tax_rule'].label_from_instance = self.taxrule_label_from_instance self.fields['tax_rule'].label_from_instance = self.taxrule_label_from_instance
if not instance.seat and not ( if not instance.seat and not (
not instance.event.settings.seating_choice and
instance.item.seat_category_mappings.filter(subevent=instance.subevent).exists() instance.item.seat_category_mappings.filter(subevent=instance.subevent).exists()
): ):
del self.fields['seat'] del self.fields['seat']
@@ -516,20 +517,6 @@ class OrderMailForm(forms.Form):
self._set_field_placeholders('message', ['event', 'order']) self._set_field_placeholders('message', ['event', 'order'])
class OrderPositionMailForm(OrderMailForm):
def __init__(self, *args, **kwargs):
position = self.position = kwargs.pop('position')
super().__init__(*args, **kwargs)
self.fields['sendto'].initial = position.attendee_email
self.fields['message'] = forms.CharField(
label=_("Message"),
required=True,
widget=forms.Textarea,
initial=self.order.event.settings.mail_text_order_custom_mail.localize(self.order.locale),
)
self._set_field_placeholders('message', ['event', 'order', 'position'])
class OrderRefundForm(forms.Form): class OrderRefundForm(forms.Form):
action = forms.ChoiceField( action = forms.ChoiceField(
required=False, required=False,
@@ -585,21 +572,7 @@ class EventCancelForm(forms.Form):
all_subevents = forms.BooleanField( all_subevents = forms.BooleanField(
label=_('Cancel all dates'), label=_('Cancel all dates'),
initial=False, initial=False,
required=False, required=False
)
subevents_from = forms.SplitDateTimeField(
widget=SplitDateTimePickerWidget(attrs={
'data-inverse-dependency': '#id_all_subevents',
}),
label=pgettext_lazy('subevent', 'All dates starting at or after'),
required=False,
)
subevents_to = forms.SplitDateTimeField(
widget=SplitDateTimePickerWidget(attrs={
'data-inverse-dependency': '#id_all_subevents',
}),
label=pgettext_lazy('subevent', 'All dates starting before'),
required=False,
) )
auto_refund = forms.BooleanField( auto_refund = forms.BooleanField(
label=_('Automatically refund money if possible'), label=_('Automatically refund money if possible'),
@@ -640,12 +613,6 @@ class EventCancelForm(forms.Form):
max_digits=10, decimal_places=2, max_digits=10, decimal_places=2,
required=False required=False
) )
keep_fee_per_ticket = forms.DecimalField(
label=_("Keep a fixed cancellation fee per ticket"),
help_text=_("Free tickets and add-on products are not counted"),
max_digits=10, decimal_places=2,
required=False
)
keep_fee_percentage = forms.DecimalField( keep_fee_percentage = forms.DecimalField(
label=_("Keep a percentual cancellation fee"), label=_("Keep a percentual cancellation fee"),
max_digits=10, decimal_places=2, max_digits=10, decimal_places=2,
@@ -750,7 +717,6 @@ class EventCancelForm(forms.Form):
self.fields['subevent'].queryset = self.event.subevents.all() self.fields['subevent'].queryset = self.event.subevents.all()
self.fields['subevent'].widget = Select2( self.fields['subevent'].widget = Select2(
attrs={ attrs={
'data-inverse-dependency': '#id_all_subevents',
'data-model-select2': 'event', 'data-model-select2': 'event',
'data-select2-url': reverse('control:event.subevents.select2', kwargs={ 'data-select2-url': reverse('control:event.subevents.select2', kwargs={
'event': self.event.slug, 'event': self.event.slug,
@@ -767,12 +733,6 @@ class EventCancelForm(forms.Form):
def clean(self): def clean(self):
d = super().clean() d = super().clean()
if d.get('subevent') and d.get('subevents_from'): if self.event.has_subevents and not d['subevent'] and not d['all_subevents']:
raise ValidationError(pgettext_lazy('subevent', 'Please either select a specific date or a date range, not both.'))
if d.get('all_subevents') and d.get('subevent_from'):
raise ValidationError(pgettext_lazy('subevent', 'Please either select all dates or a date range, not both.'))
if bool(d.get('subevents_from')) != bool(d.get('subevents_to')):
raise ValidationError(pgettext_lazy('subevent', 'If you set a date range, please set both a start and an end.'))
if self.event.has_subevents and not d['subevent'] and not d['all_subevents'] and not d.get('subevents_from'):
raise ValidationError(_('Please confirm that you want to cancel ALL dates in this event series.')) raise ValidationError(_('Please confirm that you want to cancel ALL dates in this event series.'))
return d return d

View File

@@ -4,19 +4,24 @@ from urllib.parse import urlparse
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.db.models import Q from django.db.models import Q
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _, pgettext_lazy from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes.forms import SafeModelMultipleChoiceField from django_scopes.forms import SafeModelMultipleChoiceField
from i18nfield.forms import I18nFormField, I18nTextarea
from pretix.api.models import WebHook from pretix.api.models import WebHook
from pretix.api.webhooks import get_all_webhook_events from pretix.api.webhooks import get_all_webhook_events
from pretix.base.forms import I18nModelForm, SettingsForm from pretix.base.forms import I18nModelForm, SettingsForm
from pretix.base.forms.widgets import SplitDateTimePickerWidget from pretix.base.forms.widgets import SplitDateTimePickerWidget
from pretix.base.models import Device, Gate, GiftCard, Organizer, Team from pretix.base.models import Device, Gate, GiftCard, Organizer, Team
from pretix.control.forms import ExtFileField, SplitDateTimeField from pretix.control.forms import (
ExtFileField, FontSelect, MultipleLanguagesWidget, SplitDateTimeField,
)
from pretix.control.forms.event import SafeEventMultipleChoiceField from pretix.control.forms.event import SafeEventMultipleChoiceField
from pretix.multidomain.models import KnownDomain from pretix.multidomain.models import KnownDomain
from pretix.presale.style import get_fonts
class OrganizerForm(I18nModelForm): class OrganizerForm(I18nModelForm):
@@ -213,26 +218,72 @@ class DeviceForm(forms.ModelForm):
class OrganizerSettingsForm(SettingsForm): class OrganizerSettingsForm(SettingsForm):
auto_fields = [
'organizer_info_text',
'event_list_type',
'event_list_availability',
'organizer_homepage_text',
'organizer_link_back',
'organizer_logo_image_large',
'giftcard_length',
'giftcard_expiry_years',
'locales',
'event_team_provisioning',
'primary_color',
'theme_color_success',
'theme_color_danger',
'theme_color_background',
'theme_round_borders',
'primary_font'
] organizer_info_text = I18nFormField(
label=_('Info text'),
required=False,
widget=I18nTextarea,
help_text=_('Not displayed anywhere by default, but if you want to, you can use this e.g. in ticket templates.')
)
event_team_provisioning = forms.BooleanField(
label=_('Allow creating a new team during event creation'),
help_text=_('Users that do not have access to all events under this organizer, must select one of their teams '
'to have access to the created event. This setting allows users to create an event-specified team'
' on-the-fly, even when they do not have \"Can change teams and permissions\" permission.'),
required=False,
)
primary_color = forms.CharField(
label=_("Primary color"),
required=False,
validators=[
RegexValidator(regex='^#[0-9a-fA-F]{6}$',
message=_('Please enter the hexadecimal code of a color, e.g. #990000.')),
],
widget=forms.TextInput(attrs={'class': 'colorpickerfield'})
)
theme_color_success = forms.CharField(
label=_("Accent color for success"),
help_text=_("We strongly suggest to use a shade of green."),
required=False,
validators=[
RegexValidator(regex='^#[0-9a-fA-F]{6}$',
message=_('Please enter the hexadecimal code of a color, e.g. #990000.')),
],
widget=forms.TextInput(attrs={'class': 'colorpickerfield'})
)
theme_color_danger = forms.CharField(
label=_("Accent color for errors"),
help_text=_("We strongly suggest to use a shade of red."),
required=False,
validators=[
RegexValidator(regex='^#[0-9a-fA-F]{6}$',
message=_('Please enter the hexadecimal code of a color, e.g. #990000.')),
],
widget=forms.TextInput(attrs={'class': 'colorpickerfield'})
)
theme_color_background = forms.CharField(
label=_("Page background color"),
required=False,
validators=[
RegexValidator(regex='^#[0-9a-fA-F]{6}$',
message=_('Please enter the hexadecimal code of a color, e.g. #990000.')),
],
widget=forms.TextInput(attrs={'class': 'colorpickerfield no-contrast'})
)
theme_round_borders = forms.BooleanField(
label=_("Use round edges"),
required=False,
)
organizer_homepage_text = I18nFormField(
label=_('Homepage text'),
required=False,
widget=I18nTextarea,
help_text=_('This will be displayed on the organizer homepage.')
)
organizer_logo_image = ExtFileField( organizer_logo_image = ExtFileField(
label=_('Header image'), label=_('Header image'),
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"), ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
@@ -243,6 +294,44 @@ class OrganizerSettingsForm(SettingsForm):
'can increase the size with the setting below. We recommend not using small details on the picture ' 'can increase the size with the setting below. We recommend not using small details on the picture '
'as it will be resized on smaller screens.') 'as it will be resized on smaller screens.')
) )
organizer_logo_image_large = forms.BooleanField(
label=_('Use header image in its full size'),
help_text=_('We recommend to upload a picture at least 1170 pixels wide.'),
required=False,
)
event_list_type = forms.ChoiceField(
label=_('Default overview style'),
choices=(
('list', _('List')),
('week', _('Week calendar')),
('calendar', _('Month calendar')),
)
)
event_list_availability = forms.BooleanField(
label=_('Show availability in event overviews'),
help_text=_('If checked, the list of events will show if events are sold out. This might '
'make for longer page loading times if you have lots of events and the shown status might be out '
'of date for up to two minutes.'),
required=False
)
organizer_link_back = forms.BooleanField(
label=_('Link back to organizer overview on all event pages'),
required=False
)
locales = forms.MultipleChoiceField(
choices=settings.LANGUAGES,
label=_("Use languages"),
widget=MultipleLanguagesWidget,
help_text=_('Choose all languages that your organizer homepage should be available in.')
)
primary_font = forms.ChoiceField(
label=_('Font'),
choices=[
('Open Sans', 'Open Sans')
],
widget=FontSelect,
help_text=_('Only respected by modern browsers.')
)
favicon = ExtFileField( favicon = ExtFileField(
label=_('Favicon'), label=_('Favicon'),
ext_whitelist=(".ico", ".png", ".jpg", ".gif", ".jpeg"), ext_whitelist=(".ico", ".png", ".jpg", ".gif", ".jpeg"),
@@ -251,6 +340,24 @@ class OrganizerSettingsForm(SettingsForm):
help_text=_('If you provide a favicon, we will show it instead of the default pretix icon. ' help_text=_('If you provide a favicon, we will show it instead of the default pretix icon. '
'We recommend a size of at least 200x200px to accommodate most devices.') 'We recommend a size of at least 200x200px to accommodate most devices.')
) )
giftcard_length = forms.IntegerField(
label=_('Length of gift card codes'),
help_text=_('The system generates by default {}-character long gift card codes. However, if a different length '
'is required, it can be set here.'.format(settings.ENTROPY['giftcard_secret'])),
required=False
)
giftcard_expiry_years = forms.IntegerField(
label=_('Validity of gift card codes in years'),
help_text=_('If you set a number here, gift cards will by default expire at the end of the year after this '
'many years. If you keep it empty, gift cards do not have an explicit expiry date.'),
required=False
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['primary_font'].choices += [
(a, {"title": a, "data": v}) for a, v in get_fonts().items()
]
class WebHookForm(forms.ModelForm): class WebHookForm(forms.ModelForm):

View File

@@ -305,7 +305,6 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.order.email.attachments.skipped': _('The email has been sent without attachments since they ' 'pretix.event.order.email.attachments.skipped': _('The email has been sent without attachments since they '
'would have been too large to be likely to arrive.'), 'would have been too large to be likely to arrive.'),
'pretix.event.order.email.custom_sent': _('A custom email has been sent.'), 'pretix.event.order.email.custom_sent': _('A custom email has been sent.'),
'pretix.event.order.position.email.custom_sent': _('A custom email has been sent to an attendee.'),
'pretix.event.order.email.download_reminder_sent': _('An email has been sent with a reminder that the ticket ' 'pretix.event.order.email.download_reminder_sent': _('An email has been sent with a reminder that the ticket '
'is available for download.'), 'is available for download.'),
'pretix.event.order.email.expire_warning_sent': _('An email has been sent with a warning that the order is about ' 'pretix.event.order.email.expire_warning_sent': _('An email has been sent with a warning that the order is about '
@@ -398,7 +397,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.testmode.activated': _('The shop has been taken into test mode.'), 'pretix.event.testmode.activated': _('The shop has been taken into test mode.'),
'pretix.event.testmode.deactivated': _('The test mode has been disabled.'), 'pretix.event.testmode.deactivated': _('The test mode has been disabled.'),
'pretix.event.added': _('The event has been created.'), 'pretix.event.added': _('The event has been created.'),
'pretix.event.changed': _('The event details have been changed.'), 'pretix.event.changed': _('The event settings have been changed.'),
'pretix.event.question.option.added': _('An answer option has been added to the question.'), 'pretix.event.question.option.added': _('An answer option has been added to the question.'),
'pretix.event.question.option.deleted': _('An answer option has been removed from the question.'), 'pretix.event.question.option.deleted': _('An answer option has been removed from the question.'),
'pretix.event.question.option.changed': _('An answer option has been changed.'), 'pretix.event.question.option.changed': _('An answer option has been changed.'),

View File

@@ -176,7 +176,7 @@ def get_event_navigation(request: HttpRequest):
'event': request.event.slug, 'event': request.event.slug,
'organizer': request.event.organizer.slug, 'organizer': request.event.organizer.slug,
}), }),
'active': url.url_name in ('event.orders', 'event.order', 'event.orders.search') or "event.order." in url.url_name, 'active': url.url_name in ('event.orders', 'event.order') or "event.order." in url.url_name,
}, },
{ {
'label': _('Overview'), 'label': _('Overview'),

View File

@@ -323,19 +323,3 @@ this is not an Event signal and will be called even if your plugin is not active
event if the search is performed within an event, and ``None`` otherwise. The search query will be passed as event if the search is performed within an event, and ``None`` otherwise. The search query will be passed as
``query``. ``query``.
""" """
order_search_forms = EventPluginSignal(
providing_args=['request']
)
"""
This signal allows you to return additional forms that should be rendered in the advanced order search.
You are passed ``request`` argument and are expected to return an instance of a form class that you bind
yourself when appropriate. Your form will be executed as part of the standard validation and rendering
cycle and rendered using default bootstrap styles.
You are required to set ``prefix`` on your form instance. You are required to implement a ``filter_qs(queryset)``
method on your form that returns a new, filtered query set. You are required to implement a ``filter_to_strings()``
method on your form that returns a list of strings describing the currently active filters.
As with all plugin signals, the ``sender`` keyword argument will contain the event.
"""

View File

@@ -31,7 +31,6 @@
{% bootstrap_field form.invoice_address_beneficiary layout="control" %} {% bootstrap_field form.invoice_address_beneficiary layout="control" %}
{% bootstrap_field form.invoice_address_not_asked_free layout="control" %} {% bootstrap_field form.invoice_address_not_asked_free layout="control" %}
{% bootstrap_field form.invoice_address_custom_field layout="control" %} {% bootstrap_field form.invoice_address_custom_field layout="control" %}
{% bootstrap_field form.invoice_address_explanation_text layout="control" %}
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{% trans "Issuer details" %}</legend> <legend>{% trans "Issuer details" %}</legend>
@@ -52,6 +51,7 @@
{% bootstrap_field form.invoice_additional_text layout="control" %} {% bootstrap_field form.invoice_additional_text layout="control" %}
{% bootstrap_field form.invoice_footer_text layout="control" %} {% bootstrap_field form.invoice_footer_text layout="control" %}
{% bootstrap_field form.invoice_logo_image layout="control" %} {% bootstrap_field form.invoice_logo_image layout="control" %}
{% bootstrap_field form.invoice_address_explanation_text layout="control" %}
{% bootstrap_field form.invoice_eu_currencies layout="control" %} {% bootstrap_field form.invoice_eu_currencies layout="control" %}
</fieldset> </fieldset>
</div> </div>

View File

@@ -66,7 +66,6 @@
{% bootstrap_field form.payment_term_last layout="control" %} {% bootstrap_field form.payment_term_last layout="control" %}
{% bootstrap_field form.payment_term_expire_automatically layout="control" %} {% bootstrap_field form.payment_term_expire_automatically layout="control" %}
{% bootstrap_field form.payment_term_accept_late layout="control" %} {% bootstrap_field form.payment_term_accept_late layout="control" %}
{% bootstrap_field form.payment_pending_hidden layout="control" %}
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{% trans "Advanced" %}</legend> <legend>{% trans "Advanced" %}</legend>

View File

@@ -98,7 +98,6 @@
{% bootstrap_field sform.attendee_addresses_asked layout="control" %} {% bootstrap_field sform.attendee_addresses_asked layout="control" %}
{% bootstrap_field sform.attendee_addresses_required layout="control" %} {% bootstrap_field sform.attendee_addresses_required layout="control" %}
{% bootstrap_field sform.checkout_show_copy_answers_button layout="control" %} {% bootstrap_field sform.checkout_show_copy_answers_button layout="control" %}
{% bootstrap_field sform.attendee_data_explanation_text layout="control" %}
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{% trans "Texts" %}</legend> <legend>{% trans "Texts" %}</legend>
@@ -220,7 +219,6 @@
{% if sform.event_list_type %} {% if sform.event_list_type %}
{% bootstrap_field sform.event_list_type layout="control" %} {% bootstrap_field sform.event_list_type layout="control" %}
{% endif %} {% endif %}
{% bootstrap_field form.sales_channels layout="control" %}
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{% trans "Cart" %}</legend> <legend>{% trans "Cart" %}</legend>

View File

@@ -18,135 +18,103 @@
<form action="" method="post" class="form-horizontal"> <form action="" method="post" class="form-horizontal">
{% csrf_token %} {% csrf_token %}
{% bootstrap_form_errors form %} {% bootstrap_form_errors form %}
<div class="row"> <div class="tabbed-form">
<div class="col-xs-12 col-lg-10"> <fieldset>
<div class="tabbed-form"> <legend>{% trans "General" %}</legend>
<fieldset> {% bootstrap_field form.name layout="control" %}
<legend>{% trans "General" %}</legend> {% bootstrap_field form.rate addon_after="%" layout="control" %}
{% bootstrap_field form.name layout="control" %} {% bootstrap_field form.price_includes_tax layout="control" %}
{% bootstrap_field form.rate addon_after="%" layout="control" %} </fieldset>
{% bootstrap_field form.price_includes_tax layout="control" %} <fieldset>
</fieldset> <legend>{% trans "Advanced" %}</legend>
<fieldset> <div class="alert alert-legal">
<legend>{% trans "Advanced" %}</legend> {% blocktrans trimmed with docs="https://docs.pretix.eu/en/latest/user/events/taxes.html" %}
<div class="alert alert-legal"> These settings are intended for advanced users. See the
{% blocktrans trimmed with docs="https://docs.pretix.eu/en/latest/user/events/taxes.html" %} <a href="{{ docs }}">documentation</a>
These settings are intended for advanced users. See the for more information. Note that we are not responsible for the correct handling
<a href="{{ docs }}">documentation</a> of taxes in your ticket shop. If in doubt, please contact a lawyer or tax consultant.
for more information. Note that we are not responsible for the correct handling {% endblocktrans %}
of taxes in your ticket shop. If in doubt, please contact a lawyer or tax consultant. </div>
{% endblocktrans %} {% bootstrap_field form.eu_reverse_charge layout="control" %}
</div> {% bootstrap_field form.home_country layout="control" %}
{% bootstrap_field form.eu_reverse_charge layout="control" %} <h3>{% trans "Custom taxation rules" %}</h3>
{% bootstrap_field form.home_country layout="control" %} <div class="alert alert-warning">
<h3>{% trans "Custom taxation rules" %}</h3> {% blocktrans trimmed %}
<div class="alert alert-warning"> These settings are intended for professional users with very specific taxation situations.
{% blocktrans trimmed %} If you create any rule here, the reverse charge settings above will be ignored. The rules will be
These settings are intended for professional users with very specific taxation situations. checked in order and once the first rule matches the order, it will be used and all further rules will
If you create any rule here, the reverse charge settings above will be ignored. The rules will be be ignored. If no rule matches, tax will be charged.
checked in order and once the first rule matches the order, it will be used and all further rules will {% endblocktrans %}
be ignored. If no rule matches, tax will be charged. {% trans "All of these rules will only apply if an invoice address is set." %}
{% endblocktrans %} </div>
{% trans "All of these rules will only apply if an invoice address is set." %}
</div>
<div class="formset tax-rules-formset" data-formset data-formset-prefix="{{ formset.prefix }}"> <div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
{{ formset.management_form }} {{ formset.management_form }}
{% bootstrap_formset_errors formset %} {% bootstrap_formset_errors formset %}
<script type="form-template" data-formset-empty-form> <div data-formset-body>
{% escapescript %} {% for form in formset %}
<div class="row tax-rule-line" data-formset-form> {% bootstrap_form_errors form %}
<div class="sr-only"> <div class="row" data-formset-form>
{{ formset.empty_form.id }} <div class="sr-only">
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %} {{ form.id }}
{% bootstrap_field formset.empty_form.ORDER form_group_class="" layout="inline" %} {% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
</div> </div>
<div class="col-sm-6 col-md-3 col-lg-3"> <div class="col-sm-3">
{% bootstrap_field formset.empty_form.country layout='inline' form_group_class="" %} {% bootstrap_field form.country layout='inline' form_group_class="" %}
</div> </div>
<div class="col-sm-6 col-md-3 col-lg-4"> <div class="col-sm-3">
{% bootstrap_field formset.empty_form.address_type layout='inline' form_group_class="" %} {% bootstrap_field form.address_type layout='inline' form_group_class="" %}
</div> </div>
<div class="col-sm-6 col-md-3 col-lg-3"> <div class="col-sm-3">
{% bootstrap_field formset.empty_form.action layout='inline' form_group_class="" %} {% bootstrap_field form.action layout='inline' form_group_class="" %}
</div> </div>
<div class="col-sm-6 col-md-3 col-lg-2 text-right flip"> <div class="col-sm-2">
<button type="button" class="btn btn-default" data-formset-move-up-button> {% bootstrap_field form.rate layout='inline' form_group_class="" %}
<i class="fa fa-arrow-up"></i></button> </div>
<button type="button" class="btn btn-default" data-formset-move-down-button> <div class="col-sm-1 text-right flip">
<i class="fa fa-arrow-down"></i></button> <button type="button" class="btn btn-block btn-danger" data-formset-delete-button>
<button type="button" class="btn btn-danger" data-formset-delete-button> <i class="fa fa-trash"></i></button>
<i class="fa fa-trash"></i></button>
</div>
<div class="col-sm-6 col-md-3 col-lg-4 col-md-offset-3">
{% bootstrap_field formset.empty_form.invoice_text layout='inline' form_group_class="" %}
</div>
<div class="col-sm-6 col-md-3 col-lg-3">
{% bootstrap_field formset.empty_form.rate layout='inline' form_group_class="" %}
</div>
</div>
{% endescapescript %}
</script>
<div data-formset-body class="tax-rule-lines">
{% for form in formset %}
{% bootstrap_form_errors form %}
<div class="row tax-rule-line" data-formset-form>
<div class="sr-only">
{{ form.id }}
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
{% bootstrap_field form.ORDER form_group_class="" layout="inline" %}
</div>
<div class="col-sm-6 col-md-3 col-lg-3">
{% bootstrap_field form.country layout='inline' form_group_class="" %}
</div>
<div class="col-sm-6 col-md-3 col-lg-4">
{% bootstrap_field form.address_type layout='inline' form_group_class="" %}
</div>
<div class="col-sm-6 col-md-3 col-lg-3">
{% bootstrap_field form.action layout='inline' form_group_class="" %}
</div>
<div class="col-sm-6 col-md-3 col-lg-2 text-right flip">
<button type="button" class="btn btn-default" data-formset-move-up-button>
<i class="fa fa-arrow-up"></i></button>
<button type="button" class="btn btn-default" data-formset-move-down-button>
<i class="fa fa-arrow-down"></i></button>
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
<div class="col-sm-6 col-md-3 col-lg-4 col-md-offset-3">
{% bootstrap_field form.invoice_text layout='inline' form_group_class="" %}
</div>
<div class="col-sm-6 col-md-3 col-lg-3">
{% bootstrap_field form.rate layout='inline' form_group_class="" %}
</div>
</div>
{% endfor %}
</div>
<div class="row tax-rule-line" data-formset-form>
<div class="col-sm-12">
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add a new rule" %}</button>
</div> </div>
</div> </div>
</div> {% endfor %}
</fieldset>
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</div>
<div class="col-xs-12 col-lg-2">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Change history" %}
</h3>
</div> </div>
{% include "pretixcontrol/includes/logs.html" with obj=rule %} <script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="row" data-formset-form>
<div class="sr-only">
{{ form.id }}
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-sm-3">
{% bootstrap_field formset.empty_form.country layout='inline' form_group_class="" %}
</div>
<div class="col-sm-3">
{% bootstrap_field formset.empty_form.address_type layout='inline' form_group_class="" %}
</div>
<div class="col-sm-3">
{% bootstrap_field formset.empty_form.action layout='inline' form_group_class="" %}
</div>
<div class="col-sm-2">
{% bootstrap_field formset.empty_form.rate layout='inline' form_group_class="" %}
</div>
<div class="col-sm-1 text-right flip">
<button type="button" class="btn btn-block btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
{% endescapescript %}
</script>
<p>
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add a new rule" %}</button>
</p>
</div> </div>
</div> </fieldset>
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div> </div>
</form> </form>
{% endblock %} {% endblock %}

View File

@@ -4,11 +4,11 @@
{% block form %} {% block form %}
{% bootstrap_field form.organizer layout="horizontal" %} {% bootstrap_field form.organizer layout="horizontal" %}
<div class="form-group"> <div class="form-group">
<label class="col-md-3 control-label">{% trans "Event type" %}</label> <label class="col-md-3 control-label">Event type</label>
<div class="col-md-9"> <div class="col-md-9">
<div class="big-radio radio"> <div class="big-radio radio">
<label> <label>
<input type="radio" value="" name="{{ form.has_subevents.html_name }}" {% if not form.has_subevents.value %}checked{% endif %}> <input type="radio" value="" name="{{ form.has_subevents.html_name }}">
<span class="fa fa-calendar-o"></span> <span class="fa fa-calendar-o"></span>
<strong>{% trans "Singular event or non-event shop" %}</strong><br> <strong>{% trans "Singular event or non-event shop" %}</strong><br>
<div class="help-block"> <div class="help-block">
@@ -27,7 +27,7 @@
</div> </div>
<div class="big-radio radio"> <div class="big-radio radio">
<label> <label>
<input type="radio" value="on" name="{{ form.has_subevents.html_name }}" {% if form.has_subevents.avalue %}checked{% endif %}> <input type="radio" value="on" name="{{ form.has_subevents.html_name }}">
<span class="fa fa-calendar"></span> <span class="fa fa-calendar"></span>
<strong>{% trans "Event series or time slot booking" %}</strong> <strong>{% trans "Event series or time slot booking" %}</strong>
<div class="help-block"> <div class="help-block">

View File

@@ -6,7 +6,7 @@
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data"> <form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
{% bootstrap_form_errors form %} {% bootstrap_form_errors form %}
{% bootstrap_form form layout='control' %} {% bootstrap_form form layout='horizontal' %}
<div class="form-group submit-group"> <div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save"> <button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %} {% trans "Save" %}

View File

@@ -14,84 +14,9 @@
{% bootstrap_field form.internal_name layout="control" %} {% bootstrap_field form.internal_name layout="control" %}
</div> </div>
{% bootstrap_field form.copy_from layout="control" %} {% bootstrap_field form.copy_from layout="control" %}
{% bootstrap_field form.has_variations layout="control" %}
<div class="form-group">
<label class="col-md-3 control-label">{% trans "Product type" %}</label>
<div class="col-md-9">
<div class="big-radio radio">
<label>
<input type="radio" value="on" name="{{ form.admission.html_name }}" {% if form.admission.value %}checked{% endif %}>
<span class="fa fa-user"></span>
<strong>{% trans "Admission product" %}</strong><br>
<div class="help-block">
{% blocktrans trimmed %}
Every purchase of this product represents one person who is allowed to enter your event.
By default, pretix will only ask for attendee information and offer ticket downloads for these products.
{% endblocktrans %}
</div>
<div class="help-block">
{% blocktrans trimmed %}
This option should be set for most things that you would call a "ticket". For product add-ons or bundles, this should
be set on the main ticket, except if the add-on products or bundled products represent additional people (e.g. group bundles).
{% endblocktrans %}
</div>
</label>
</div>
<div class="big-radio radio">
<label>
<input type="radio" value="" name="{{ form.admission.html_name }}" {% if not form.admission.value %}checked{% endif %}>
<span class="fa fa-cube"></span>
<strong>{% trans "Non-admission product" %}</strong>
<div class="help-block">
{% blocktrans trimmed %}
A product that does not represent a person. By default, pretix will not ask for attendee information or offer
ticket downloads.
{% endblocktrans %}
</div>
<div class="help-block">
{% blocktrans trimmed %}
Examples: Merchandise, donations, gift cards, add-ons to a main ticket.
{% endblocktrans %}
</div>
</label>
</div>
</div>
</div>
{% bootstrap_field form.category layout="control" %} {% bootstrap_field form.category layout="control" %}
{% bootstrap_field form.admission layout="control" %}
<div class="form-group">
<label class="col-md-3 control-label">{% trans "Product variations" %}</label>
<div class="col-md-9">
<div class="big-radio radio">
<label>
<input type="radio" value="" name="{{ form.has_variations.html_name }}" {% if not form.has_variations.value %}checked{% endif %}>
<span class="fa fa-fw fa-square"></span>
<strong>{% trans "Product without variations" %}</strong><br>
</label>
</div>
<div class="big-radio radio">
<label>
<input type="radio" value="on" name="{{ form.has_variations.html_name }}" {% if form.has_variations.value %}checked{% endif %}>
<span class="fa fa-fw fa-th-large"></span>
<strong>{% trans "Product with multiple variations" %}</strong>
<div class="help-block">
{% blocktrans trimmed %}
This product exists in multiple variations which are different in either their name, price, quota, or description.
All other settings need to be the same.
{% endblocktrans %}
</div>
<div class="help-block">
{% blocktrans trimmed %}
Examples: Ticket category with variations for "full price" and "reduced", merchandise with variations for different sizes,
workshop add-on with variations for simultaneous workshops.
{% endblocktrans %}
</div>
</label>
</div>
</div>
</div>
</fieldset> </fieldset>
{% if form.quota_option %} {% if form.quota_option %}
<fieldset> <fieldset>

View File

@@ -10,56 +10,13 @@
<div class="tabbed-form"> <div class="tabbed-form">
<fieldset> <fieldset>
<legend>{% trans "General" %}</legend> <legend>{% trans "General" %}</legend>
{% bootstrap_field form.active layout="control" %}
{% bootstrap_field form.name layout="control" %} {% bootstrap_field form.name layout="control" %}
<div class="internal-name-wrapper"> <div class="internal-name-wrapper">
{% bootstrap_field form.internal_name layout="control" %} {% bootstrap_field form.internal_name layout="control" %}
</div> </div>
{% bootstrap_field form.category layout="control" %} {% bootstrap_field form.category layout="control" %}
{% bootstrap_field form.active layout="control" %}
<div class="form-group"> {% bootstrap_field form.admission layout="control" %}
<label class="col-md-3 control-label">{% trans "Product type" %}</label>
<div class="col-md-9">
<div class="big-radio radio">
<label>
<input type="radio" value="on" name="{{ form.admission.html_name }}" {% if form.admission.value %}checked{% endif %}>
<span class="fa fa-fw fa-user"></span>
<strong>{% trans "Admission product" %}</strong><br>
<div class="help-block">
{% blocktrans trimmed %}
Every purchase of this product represents one person who is allowed to enter your event.
By default, pretix will only ask for attendee information and offer ticket downloads for these products.
{% endblocktrans %}
</div>
<div class="help-block">
{% blocktrans trimmed %}
This option should be set for most things that you would call a "ticket". For product add-ons or bundles, this should
be set on the main ticket, except if the add-on products or bundled products represent additional people (e.g. group bundles).
{% endblocktrans %}
</div>
</label>
</div>
<div class="big-radio radio">
<label>
<input type="radio" value="" name="{{ form.admission.html_name }}" {% if not form.admission.value %}checked{% endif %}>
<span class="fa fa-fw fa-cube"></span>
<strong>{% trans "Non-admission product" %}</strong>
<div class="help-block">
{% blocktrans trimmed %}
A product that does not represent a person. By default, pretix will not ask for attendee information or offer
ticket downloads.
{% endblocktrans %}
</div>
<div class="help-block">
{% blocktrans trimmed %}
Examples: Merchandise, donations, gift cards, add-ons to a main ticket.
{% endblocktrans %}
</div>
</label>
</div>
</div>
</div>
{% bootstrap_field form.description layout="control" %} {% bootstrap_field form.description layout="control" %}
{% bootstrap_field form.picture layout="control" %} {% bootstrap_field form.picture layout="control" %}
{% bootstrap_field form.require_approval layout="control" %} {% bootstrap_field form.require_approval layout="control" %}

View File

@@ -73,7 +73,7 @@
</td> </td>
<td> <td>
{% if i.var_count %} {% if i.var_count %}
<span class="fa fa-th-large fa-fw text-muted" data-toggle="tooltip" title="{% trans "Product with variations" %}"></span> <span class="fa fa-list-ul fa-fw text-muted" data-toggle="tooltip" title="{% trans "Product with variations" %}"></span>
{% endif %} {% endif %}
</td> </td>
<td> <td>

View File

@@ -32,18 +32,6 @@
accepted. If you want to allow both options, do not make this field required. accepted. If you want to allow both options, do not make this field required.
{% endblocktrans %} {% endblocktrans %}
</div> </div>
<div id="valid-number">
{% bootstrap_field form.valid_number_min layout="control" %}
{% bootstrap_field form.valid_number_max layout="control" %}
</div>
<div id="valid-date">
{% bootstrap_field form.valid_date_min layout="control" %}
{% bootstrap_field form.valid_date_max layout="control" %}
</div>
<div id="valid-datetime">
{% bootstrap_field form.valid_datetime_min layout="control" %}
{% bootstrap_field form.valid_datetime_max layout="control" %}
</div>
<div id="answer-options"> <div id="answer-options">
<h3>{% trans "Answer options" %}</h3> <h3>{% trans "Answer options" %}</h3>
<noscript> <noscript>

View File

@@ -182,7 +182,7 @@
{{ order.email|default_if_none:"" }} {{ order.email|default_if_none:"" }}
{% if order.email and order.email_known_to_work %} {% if order.email and order.email_known_to_work %}
<span class="fa fa-check-circle text-success" data-toggle="tooltip" title="{% trans "We know that this email address works because the user clicked a link we sent them." %}"></span> <span class="fa fa-check-circle text-success" data-toggle="tooltip" title="{% trans "We know that this email address works because the user clicked a link we sent them." %}"></span>
{% endif %} {% endif %}&nbsp;&nbsp;
<a href="{% url "control:event.order.contact" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs"> <a href="{% url "control:event.order.contact" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
<span class="fa fa-edit"></span> <span class="fa fa-edit"></span>
</a> </a>
@@ -268,12 +268,12 @@
<div class="panel panel-default items"> <div class="panel panel-default items">
<div class="panel-heading"> <div class="panel-heading">
<div class="pull-right flip"> <div class="pull-right flip">
<a href="{% url "control:event.order.info" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
<span class="fa fa-edit"></span>
{% trans "Change answers" %}
</a>
{% if order.changable and 'can_change_orders' in request.eventpermset %} {% if order.changable and 'can_change_orders' in request.eventpermset %}
&middot; <a href="{% url "control:event.order.change" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}"> <a href="{% url "control:event.order.info" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
<span class="fa fa-edit"></span>
{% trans "Change answers" %}
</a> &middot;
<a href="{% url "control:event.order.change" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
<span class="fa fa-edit"></span> <span class="fa fa-edit"></span>
{% trans "Change products" %} {% trans "Change products" %}
</a> </a>
@@ -388,12 +388,8 @@
{{ line.attendee_email }} {{ line.attendee_email }}
{% if not line.addon_to %} {% if not line.addon_to %}
<form class="form-inline helper-display-inline" method="post" <form class="form-inline helper-display-inline" method="post"
action="{% url "control:event.order.resendlink" event=request.event.slug organizer=request.event.organizer.slug code=order.code position=line.pk %}"> action="{% url "control:event.order.resendlink" event=request.event.slug organizer=request.event.organizer.slug code=order.code position=line.pk %}">
{% csrf_token %} {% csrf_token %}
<a href="{% url "control:event.order.position.sendmail" event=request.event.slug organizer=request.event.organizer.slug code=order.code position=line.pk %}"
class="btn btn-default btn-xs">
<span class="fa fa-envelope-o"></span>
</a>
<button class="btn btn-default btn-xs"> <button class="btn btn-default btn-xs">
{% trans "Resend link" %} {% trans "Resend link" %}
</button> </button>

View File

@@ -25,30 +25,22 @@
<strong>{{ pending }}</strong>. The order total is <strong>{{ total }}</strong>. <strong>{{ pending }}</strong>. The order total is <strong>{{ total }}</strong>.
{% endblocktrans %} {% endblocktrans %}
</p> </p>
{% if order.status == "c" or order.positions.count == 0 %} <p>
<p> {% blocktrans trimmed with amount=refund.amount|money:request.event.currency method=refund.payment_provider.verbose_name %}
{% blocktrans trimmed %} What should happen to the ticket order?
Since the order is already canceled, this will not affect its state. {% endblocktrans %}
{% endblocktrans %} </p>
</p> <div class="form-inline">
{% else %} <label class="radio">
<p> <input type="radio" name="action" value="n" {% if not propose_cancel %}checked{% endif %}>
{% blocktrans trimmed with amount=refund.amount|money:request.event.currency method=refund.payment_provider.verbose_name %} {% trans "Mark the order as unpaid and allow the customer to pay again with another payment method." %}
What should happen to the ticket order? </label>
{% endblocktrans %} <br>
</p> <label class="radio">
<div class="form-inline"> <input type="radio" name="action" value="r" {% if propose_cancel %}checked{% endif %}>
<label class="radio"> {% trans "Cancel the order irrevocably." %}
<input type="radio" name="action" value="n" {% if not propose_cancel %}checked{% endif %}> </label>
{% trans "Mark the order as unpaid and allow the customer to pay again with another payment method." %} </div>
</label>
<br>
<label class="radio">
<input type="radio" name="action" value="r" {% if propose_cancel %}checked{% endif %}>
{% trans "Cancel the order irrevocably." %}
</label>
</div>
{% endif %}
<div class="form-group submit-group"> <div class="form-group submit-group">
<a class="btn btn-default btn-lg" <a class="btn btn-default btn-lg"
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}"> href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">

View File

@@ -27,10 +27,8 @@
{% if request.event.has_subevents %} {% if request.event.has_subevents %}
<fieldset> <fieldset>
<legend>{% trans "Select date" context "subevents" %}</legend> <legend>{% trans "Select date" context "subevents" %}</legend>
{% bootstrap_field form.all_subevents layout="control" %}
{% bootstrap_field form.subevent layout="control" %} {% bootstrap_field form.subevent layout="control" %}
{% bootstrap_field form.subevents_from layout="control" %} {% bootstrap_field form.all_subevents layout="control" %}
{% bootstrap_field form.subevents_to layout="control" %}
</fieldset> </fieldset>
{% endif %} {% endif %}
<fieldset> <fieldset>
@@ -41,7 +39,6 @@
{% bootstrap_field form.gift_card_expires layout="control" %} {% bootstrap_field form.gift_card_expires layout="control" %}
{% bootstrap_field form.gift_card_conditions layout="control" %} {% bootstrap_field form.gift_card_conditions layout="control" %}
{% bootstrap_field form.keep_fee_fixed layout="control" %} {% bootstrap_field form.keep_fee_fixed layout="control" %}
{% bootstrap_field form.keep_fee_per_ticket layout="control" %}
{% bootstrap_field form.keep_fee_percentage layout="control" %} {% bootstrap_field form.keep_fee_percentage layout="control" %}
{% bootstrap_field form.keep_fees layout="control" %} {% bootstrap_field form.keep_fees layout="control" %}
</fieldset> </fieldset>

View File

@@ -2,36 +2,19 @@
{% load bootstrap3 %} {% load bootstrap3 %}
{% if order.status == "n" %} {% if order.status == "n" %}
{% if order.require_approval %} {% if order.require_approval %}
<span class="label label-warning {{ class }}"> <span class="label label-warning {{ class }}">{% trans "Approval pending" %}</span>
<span class="fa fa-question-circle"></span>
{% trans "Approval pending" %}
</span>
{% else %} {% else %}
<span data-toggle="tooltip" title="{{ order.expires|date:"SHORT_DATETIME_FORMAT" }}" <span data-toggle="tooltip" title="{{ order.expires|date:"SHORT_DATETIME_FORMAT" }}"
class="label label-warning {{ class }}"> class="label label-warning {{ class }}">{% trans "Pending" %}</span>
<span class="fa fa-money"></span>
{% trans "Pending" %}
</span>
{% endif %} {% endif %}
{% elif order.status == "p" %} {% elif order.status == "p" %}
{% if order.count_positions == 0 %} {% if order.count_positions == 0 %}
<span class="label label-info {{ class }}"> <span class="label label-info {{ class }}">{% trans "Canceled (paid fee)" %}</span>
<span class="fa fa-times"></span>
{% trans "Canceled (paid fee)" %}
</span>
{% else %} {% else %}
<span class="label label-success {{ class }}"> <span class="label label-success {{ class }}">{% trans "Paid" %}</span>
<span class="fa fa-check"></span>
{% trans "Paid" %}
</span>
{% endif %} {% endif %}
{% elif order.status == "e" %} {# expired #} {% elif order.status == "e" %} {# expired #}
<span class="label label-danger {{ class }}"> <span class="label label-danger {{ class }}">{% trans "Expired" %}</span>
<span class="fa fa-clock-o"></span>
{% trans "Expired" %}</span>
{% elif order.status == "c" %} {% elif order.status == "c" %}
<span class="label label-danger {{ class }}"> <span class="label label-danger {{ class }}">{% trans "Canceled" %}</span>
<span class="fa fa-times"></span>
{% trans "Canceled" %}
</span>
{% endif %} {% endif %}

View File

@@ -7,7 +7,7 @@
{% block title %}{% trans "Orders" %}{% endblock %} {% block title %}{% trans "Orders" %}{% endblock %}
{% block content %} {% block content %}
<h1>{% trans "Orders" %}</h1> <h1>{% trans "Orders" %}</h1>
{% if not filter_form.filtered and orders|length == 0 and not filter_strings %} {% if not filter_form.filtered and orders|length == 0 %}
<div class="empty-collection"> <div class="empty-collection">
<p> <p>
{% blocktrans trimmed %} {% blocktrans trimmed %}
@@ -21,72 +21,57 @@
{% trans "Take your shop live" %} {% trans "Take your shop live" %}
</a> </a>
{% else %} {% else %}
<a href="{% eventurl request.event "presale:event.index" %}" class="btn btn-primary btn-lg" target="_blank"> <a href="{% eventurl request.event "presale:event.index" %}" class="btn btn-primary btn-lg">
{% trans "Go to the ticket shop" %} {% trans "Go to the ticket shop" %}
</a> </a>
{% endif %} {% endif %}
</div> </div>
{% else %} {% else %}
{% if filter_strings %} <div class="row filter-form">
<p> <form class="col-md-2 col-xs-12"
<span class="fa fa-filter"></span> action="{% url "control:event.orders.go" event=request.event.slug organizer=request.event.organizer.slug %}">
{% trans "Search query:" %} <div class="input-group">
{{ filter_strings|join:" · " }} <input type="text" name="code" class="form-control" placeholder="{% trans "Order code" %}" autofocus>
· <span class="input-group-btn">
<a href="{% url "control:event.orders.search" event=request.event.slug organizer=request.event.organizer.slug %}?{{ request.META.QUERY_STRING }}"> <button class="btn btn-primary" type="submit">{% trans "Go!" %}</button>
<span class="fa fa-edit"></span> </span>
{% trans "Edit" %} </div>
</a> </form>
</p> <form class="" action="" method="get">
{% else %} <div class="col-md-2 col-xs-6">
<div class="row filter-form"> {% bootstrap_field filter_form.status layout='inline' %}
<form class="col-md-2 col-xs-12" </div>
action="{% url "control:event.orders.go" event=request.event.slug organizer=request.event.organizer.slug %}"> {% if request.event.has_subevents %}
<div class="input-group"> <div class="col-md-1 col-xs-6">
<input type="text" name="code" class="form-control" placeholder="{% trans "Order code" %}" autofocus> {% bootstrap_field filter_form.item layout='inline' %}
<span class="input-group-btn"> </div>
<button class="btn btn-primary" type="submit">{% trans "Go!" %}</button> <div class="col-md-2 col-xs-6">
{% bootstrap_field filter_form.subevent layout='inline' %}
</div>
<div class="col-md-1 col-xs-6">
{% bootstrap_field filter_form.provider layout='inline' %}
</div>
{% else %}
<div class="col-md-2 col-xs-6">
{% bootstrap_field filter_form.item layout='inline' %}
</div>
<div class="col-md-2 col-xs-6">
{% bootstrap_field filter_form.provider layout='inline' %}
</div>
{% endif %}
<div class="col-md-2 col-xs-6">
{% bootstrap_field filter_form.query layout='inline' %}
</div>
<div class="col-md-2 col-xs-6">
<button class="btn btn-primary btn-block" type="submit">
<span class="fa fa-filter"></span>
<span class="hidden-md">
{% trans "Filter" %}
</span> </span>
</div> </button>
</form> </div>
<form class="" action="" method="get"> </form>
<div class="col-md-2 col-xs-6"> </div>
{% bootstrap_field filter_form.status layout='inline' %}
</div>
{% if request.event.has_subevents %}
<div class="col-md-1 col-xs-6">
{% bootstrap_field filter_form.item layout='inline' %}
</div>
<div class="col-md-2 col-xs-6">
{% bootstrap_field filter_form.subevent layout='inline' %}
</div>
<div class="col-md-1 col-xs-6">
{% bootstrap_field filter_form.provider layout='inline' %}
</div>
{% else %}
<div class="col-md-2 col-xs-6">
{% bootstrap_field filter_form.item layout='inline' %}
</div>
<div class="col-md-2 col-xs-6">
{% bootstrap_field filter_form.provider layout='inline' %}
</div>
{% endif %}
<div class="col-md-2 col-xs-6">
{% bootstrap_field filter_form.query layout='inline' %}
</div>
<div class="col-md-1 col-xs-6">
<button class="btn btn-primary btn-block" type="submit">
<span class="fa fa-filter"></span>
</button>
</div>
<div class="col-md-1 col-xs-6">
<a href="{% url "control:event.orders.search" event=request.event.slug organizer=request.event.organizer.slug %}" class="btn btn-default btn-block" type="submit" data-toggle="tooltip" title="{% trans "Advanced search" %}">
<span class="fa fa-cog"></span>
</a>
</div>
</form>
</div>
{% endif %}
{% if filter_form.is_valid and filter_form.cleaned_data.question %} {% if filter_form.is_valid and filter_form.cleaned_data.question %}
<p class="text-muted"> <p class="text-muted">
<span class="fa fa-filter"></span> <span class="fa fa-filter"></span>

View File

@@ -12,14 +12,7 @@
<button type="button" data-target=".sum-net" class="btn btn-default">{% trans "Revenue (net)" %}</button> <button type="button" data-target=".sum-net" class="btn btn-default">{% trans "Revenue (net)" %}</button>
</div> </div>
</div> </div>
<h1> <h1>{% trans "Order overview" %}</h1>
{% trans "Order overview" %}
<a href="{% url "control:event.orders.export" event=request.event.slug organizer=request.event.organizer.slug %}?identifier=pdfreport"
class="btn btn-default" target="_blank">
<span class="fa fa-download"></span>
{% trans "PDF" %}
</a>
</h1>
<div class="row filter-form"> <div class="row filter-form">
<form class="" action="" method="get"> <form class="" action="" method="get">
{% if request.event.has_subevents %} {% if request.event.has_subevents %}
@@ -65,14 +58,12 @@
<th>{% trans "Product" %}</th> <th>{% trans "Product" %}</th>
<th>{% trans "Canceled" %}¹</th> <th>{% trans "Canceled" %}¹</th>
<th>{% trans "Expired" %}</th> <th>{% trans "Expired" %}</th>
<th>{% trans "Approval pending" %}</th>
<th colspan="3" class="text-center">{% trans "Purchased" %}</th> <th colspan="3" class="text-center">{% trans "Purchased" %}</th>
</tr> </tr>
<tr> <tr>
<th></th> <th></th>
<th></th> <th></th>
<th></th> <th></th>
<th></th>
<th>{% trans "Pending" %}</th> <th>{% trans "Pending" %}</th>
<th>{% trans "Paid" %}</th> <th>{% trans "Paid" %}</th>
<th>{% trans "Total" %}</th> <th>{% trans "Total" %}</th>
@@ -85,7 +76,6 @@
<th>{{ tup.0 }}</th> <th>{{ tup.0 }}</th>
<th>{{ tup.0.num.canceled|togglesum:request.event.currency }}</th> <th>{{ tup.0.num.canceled|togglesum:request.event.currency }}</th>
<th>{{ tup.0.num.expired|togglesum:request.event.currency }}</th> <th>{{ tup.0.num.expired|togglesum:request.event.currency }}</th>
<th>{{ tup.0.num.unapproved|togglesum:request.event.currency }}</th>
<th>{{ tup.0.num.pending|togglesum:request.event.currency }}</th> <th>{{ tup.0.num.pending|togglesum:request.event.currency }}</th>
<th>{{ tup.0.num.paid|togglesum:request.event.currency }}</th> <th>{{ tup.0.num.paid|togglesum:request.event.currency }}</th>
<th>{{ tup.0.num.total|togglesum:request.event.currency }}</th> <th>{{ tup.0.num.total|togglesum:request.event.currency }}</th>
@@ -105,12 +95,7 @@
</a> </a>
</td> </td>
<td> <td>
<a href="{{ listurl }}?item={{ item.id }}&amp;status=pa&amp;provider={{ item.provider }}"> <a href="{{ listurl }}?item={{ item.id }}&amp;status=n&amp;provider={{ item.provider }}">
{{ item.num.unapproved|togglesum:request.event.currency }}
</a>
</td>
<td>
<a href="{{ listurl }}?item={{ item.id }}&amp;status=na&amp;provider={{ item.provider }}">
{{ item.num.pending|togglesum:request.event.currency }} {{ item.num.pending|togglesum:request.event.currency }}
</a> </a>
</td> </td>
@@ -138,12 +123,7 @@
</a> </a>
</td> </td>
<td> <td>
<a href="{{ listurl }}?item={{ item.id }}-{{ var.id }}&amp;status=pa&amp;provider={{ item.provider }}"> <a href="{{ listurl }}?item={{ item.id }}-{{ var.id }}&amp;status=n&amp;provider={{ item.provider }}">
{{ var.num.unapproved|togglesum:request.event.currency }}
</a>
</td>
<td>
<a href="{{ listurl }}?item={{ item.id }}-{{ var.id }}&amp;status=na&amp;provider={{ item.provider }}">
{{ var.num.pending|togglesum:request.event.currency }} {{ var.num.pending|togglesum:request.event.currency }}
</a> </a>
</td> </td>
@@ -166,7 +146,6 @@
<th>{% trans "Total" %}</th> <th>{% trans "Total" %}</th>
<th>{{ total.num.canceled|togglesum:request.event.currency }}</th> <th>{{ total.num.canceled|togglesum:request.event.currency }}</th>
<th>{{ total.num.expired|togglesum:request.event.currency }}</th> <th>{{ total.num.expired|togglesum:request.event.currency }}</th>
<th>{{ total.num.unapproved|togglesum:request.event.currency }}</th>
<th>{{ total.num.pending|togglesum:request.event.currency }}</th> <th>{{ total.num.pending|togglesum:request.event.currency }}</th>
<th>{{ total.num.paid|togglesum:request.event.currency }}</th> <th>{{ total.num.paid|togglesum:request.event.currency }}</th>
<th>{{ total.num.total|togglesum:request.event.currency }}</th> <th>{{ total.num.total|togglesum:request.event.currency }}</th>

View File

@@ -1,23 +0,0 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load eventurl %}
{% load urlreplace %}
{% load money %}
{% load bootstrap3 %}
{% block title %}{% trans "Order search" %}{% endblock %}
{% block content %}
<h1>{% trans "Order search" %}</h1>
<form class="form-horizontal" action="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}" method="get">
{% for f in forms %}
{% bootstrap_form_errors f layout='control' %}
{% for field in f %}
{% bootstrap_field field layout='control' %}
{% endfor %}
{% endfor %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Search" %}
</button>
</div>
</form>
{% endblock %}

Some files were not shown because too many files have changed in this diff Show More