mirror of
https://github.com/pretix/pretix.git
synced 2025-12-12 04:42:28 +00:00
Compare commits
145 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbae184a6f | ||
|
|
41ef66917d | ||
|
|
20dbb3dfd0 | ||
|
|
53c71669b9 | ||
|
|
aa13a927e1 | ||
|
|
6dea8a0e0f | ||
|
|
13441bf3c4 | ||
|
|
0291676e2d | ||
|
|
19e5843d99 | ||
|
|
4ede99c04b | ||
|
|
987802335b | ||
|
|
eb7e272273 | ||
|
|
2761419952 | ||
|
|
4b422571ad | ||
|
|
c340fd9d97 | ||
|
|
e5d554a7b3 | ||
|
|
076aa097f6 | ||
|
|
97b9c1029a | ||
|
|
2ebd040a7c | ||
|
|
14a66ff80c | ||
|
|
76c6bbc321 | ||
|
|
0272e44edd | ||
|
|
99d2c40935 | ||
|
|
2720cf5ae1 | ||
|
|
3e415c2654 | ||
|
|
6d1ad45908 | ||
|
|
5514279868 | ||
|
|
868aae0054 | ||
|
|
55f89b2125 | ||
|
|
10e0e9e618 | ||
|
|
1119f90c02 | ||
|
|
35108c0e47 | ||
|
|
86b722015f | ||
|
|
54e9a03b9a | ||
|
|
c90365e908 | ||
|
|
5c85c69b3d | ||
|
|
6d9e1be844 | ||
|
|
168a6bae98 | ||
|
|
6c1fa8cf2d | ||
|
|
88be280445 | ||
|
|
6aa3532ee6 | ||
|
|
b8db58b978 | ||
|
|
5a95550075 | ||
|
|
627f601bdb | ||
|
|
6c03e49090 | ||
|
|
0d0294a292 | ||
|
|
d389a2aaa1 | ||
|
|
f51ec04e05 | ||
|
|
023f9eb6e7 | ||
|
|
0bd1c3f3af | ||
|
|
821599dc1a | ||
|
|
9a65ad0abe | ||
|
|
12cb555917 | ||
|
|
87656cef4c | ||
|
|
3a67203a0d | ||
|
|
695a800811 | ||
|
|
e3c820b760 | ||
|
|
c52bf0be8c | ||
|
|
b287f870b1 | ||
|
|
48f3a157bc | ||
|
|
62a0dd2541 | ||
|
|
8c63f2159c | ||
|
|
e5a77dc482 | ||
|
|
bd81d7dced | ||
|
|
23c38a3742 | ||
|
|
6c29fc0117 | ||
|
|
eae1fc9a81 | ||
|
|
2c1195eaa1 | ||
|
|
f94e8e5bdc | ||
|
|
20ec388b03 | ||
|
|
02278660bc | ||
|
|
01b90ded36 | ||
|
|
10b592a1c4 | ||
|
|
cfffcf2d1a | ||
|
|
df83682d55 | ||
|
|
eeb3c1a960 | ||
|
|
a7565342c0 | ||
|
|
d03c5ce30c | ||
|
|
b51108ab22 | ||
|
|
d08c811f3a | ||
|
|
c757f3e4c7 | ||
|
|
5962e4d4ab | ||
|
|
6fd2662956 | ||
|
|
259d2cdb27 | ||
|
|
04e9c8a226 | ||
|
|
78798ff382 | ||
|
|
be1926ff21 | ||
|
|
6af5b3fd5e | ||
|
|
8989723145 | ||
|
|
e980b2c255 | ||
|
|
cb0023dc3c | ||
|
|
b4c18c6ea6 | ||
|
|
e07cca9148 | ||
|
|
031ee647ab | ||
|
|
6ca6f9437f | ||
|
|
07ff523ea3 | ||
|
|
92df47d0c7 | ||
|
|
717c905d16 | ||
|
|
e922bd7376 | ||
|
|
a48d844456 | ||
|
|
48119038b4 | ||
|
|
598f0b316e | ||
|
|
7df503fb4f | ||
|
|
4c84cf7b37 | ||
|
|
f969db69cb | ||
|
|
fb92676aee | ||
|
|
6052895ada | ||
|
|
7a98f3fa89 | ||
|
|
da149682aa | ||
|
|
ba4eff5545 | ||
|
|
32c08d431f | ||
|
|
ecd914f44d | ||
|
|
f6dc90fb28 | ||
|
|
4093c1d909 | ||
|
|
9da14dfebe | ||
|
|
a941378b80 | ||
|
|
9202aca26a | ||
|
|
b841878dcb | ||
|
|
2cf6a4a6ab | ||
|
|
8759155357 | ||
|
|
1fe4d1a8ca | ||
|
|
73e0937d80 | ||
|
|
151d5c4f2b | ||
|
|
8486f66e69 | ||
|
|
9bb8f7b429 | ||
|
|
53ce1a53c6 | ||
|
|
ce61c8a23a | ||
|
|
13f825ec1b | ||
|
|
4ff4402a5f | ||
|
|
b4964b1460 | ||
|
|
710aaa5f1c | ||
|
|
ed12fd3cd5 | ||
|
|
ec7be3bd07 | ||
|
|
95aa7b7619 | ||
|
|
f9d1dc7181 | ||
|
|
ad094bcfc0 | ||
|
|
2b1d9bc039 | ||
|
|
762d815cf5 | ||
|
|
6a71b9bf19 | ||
|
|
d2617ca104 | ||
|
|
a3573125df | ||
|
|
565a65f780 | ||
|
|
9543d89014 | ||
|
|
e61288ba67 | ||
|
|
58af025fd8 |
12
Dockerfile
12
Dockerfile
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.6
|
FROM python:3.8
|
||||||
|
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
@@ -30,7 +30,8 @@ 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
|
||||||
@@ -47,12 +48,13 @@ 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 \
|
||||||
-r requirements/redis.txt \
|
gunicorn django-extensions ipython && \
|
||||||
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.conf /etc/supervisord.conf
|
COPY deployment/docker/supervisord /etc/supervisord
|
||||||
|
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
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ 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
|
||||||
@@ -16,10 +18,16 @@ 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.conf
|
exec sudo -E /usr/bin/supervisord -n -c /etc/supervisord.all.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
|
||||||
@@ -37,10 +45,6 @@ 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
|
||||||
|
|||||||
2
deployment/docker/supervisord.all.conf
Normal file
2
deployment/docker/supervisord.all.conf
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[include]
|
||||||
|
files = /etc/supervisord/*.conf
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
[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
|
|
||||||
2
deployment/docker/supervisord.web.conf
Normal file
2
deployment/docker/supervisord.web.conf
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[include]
|
||||||
|
files = /etc/supervisord/base.conf /etc/supervisord/nginx.conf /etc/supervisord/pretixweb.conf
|
||||||
18
deployment/docker/supervisord/base.conf
Normal file
18
deployment/docker/supervisord/base.conf
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[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
|
||||||
7
deployment/docker/supervisord/nginx.conf
Normal file
7
deployment/docker/supervisord/nginx.conf
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[program:nginx]
|
||||||
|
command=/usr/sbin/nginx
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
priority=10
|
||||||
|
stdout_events_enabled=true
|
||||||
|
stderr_events_enabled=true
|
||||||
6
deployment/docker/supervisord/pretixtask.conf
Normal file
6
deployment/docker/supervisord/pretixtask.conf
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[program:pretixtask]
|
||||||
|
command=/usr/local/bin/pretix taskworker
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
priority=5
|
||||||
|
user=pretixuser
|
||||||
7
deployment/docker/supervisord/pretixweb.conf
Normal file
7
deployment/docker/supervisord/pretixweb.conf
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[program:pretixweb]
|
||||||
|
command=/usr/local/bin/pretix webworker
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
priority=5
|
||||||
|
user=pretixuser
|
||||||
|
environment=HOME=/pretix
|
||||||
@@ -23,6 +23,14 @@ 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
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
|
|||||||
@@ -284,6 +284,24 @@ 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/
|
||||||
|
|||||||
215
doc/api/resources/exporters.rst
Normal file
215
doc/api/resources/exporters.rst
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
.. 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.
|
||||||
|
|
||||||
@@ -27,5 +27,6 @@ Resources and endpoints
|
|||||||
devices
|
devices
|
||||||
webhooks
|
webhooks
|
||||||
seatingplans
|
seatingplans
|
||||||
|
exporters
|
||||||
billing_invoices
|
billing_invoices
|
||||||
billing_var
|
billing_var
|
||||||
|
|||||||
@@ -163,6 +163,10 @@ 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:
|
||||||
|
|
||||||
@@ -490,7 +494,8 @@ 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.
|
: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_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
|
||||||
|
|||||||
@@ -31,8 +31,10 @@ 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``
|
||||||
@@ -42,6 +44,12 @@ 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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
item_formsets, order_search_filter_q, order_search_forms
|
||||||
|
|
||||||
.. 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
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "3.12.1"
|
__version__ = "3.13.1"
|
||||||
|
|||||||
@@ -108,6 +108,10 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
|
|||||||
('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'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
127
src/pretix/api/serializers/exporters.py
Normal file
127
src/pretix/api/serializers/exporters.py
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
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
|
||||||
@@ -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, item, oauth, order, organizer, user, version,
|
checkin, device, event, exporters, item, oauth, order, organizer, user,
|
||||||
voucher, waitinglist, webhooks,
|
version, voucher, waitinglist, webhooks,
|
||||||
)
|
)
|
||||||
|
|
||||||
router = routers.DefaultRouter()
|
router = routers.DefaultRouter()
|
||||||
@@ -22,6 +22,7 @@ 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)
|
||||||
@@ -44,6 +45,7 @@ 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')
|
||||||
|
|||||||
154
src/pretix/api/views/exporters.py
Normal file
154
src/pretix/api/views/exporters.py
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
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(web_download=False)
|
||||||
|
cf.date = now()
|
||||||
|
cf.expires = now() + timedelta(hours=24)
|
||||||
|
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
|
||||||
|
})
|
||||||
@@ -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,
|
||||||
TeamAPIToken, generate_secret,
|
TaxRule, 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,6 +65,7 @@ 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:
|
||||||
@@ -84,6 +85,19 @@ 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:
|
||||||
@@ -547,7 +561,10 @@ class OrderViewSet(viewsets.ModelViewSet):
|
|||||||
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:
|
||||||
|
|||||||
@@ -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 _
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
from django_scopes import scope, scopes_disabled
|
from django_scopes import scope, scopes_disabled
|
||||||
from requests import RequestException
|
from requests import RequestException
|
||||||
|
|
||||||
@@ -97,6 +97,67 @@ 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):
|
||||||
@@ -169,32 +230,57 @@ 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_id: int):
|
def notify_webhooks(logentry_ids: list):
|
||||||
logentry = LogEntry.all.select_related('event', 'event__organizer').get(id=logentry_id)
|
if not isinstance(logentry_ids, list):
|
||||||
|
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:
|
if not logentry.organizer:
|
||||||
return # We need to know the organizer
|
break # We need to know the organizer
|
||||||
|
|
||||||
types = get_all_webhook_events()
|
notification_type = logentry.webhook_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 not notification_type:
|
if not notification_type:
|
||||||
return # Ignore, no webhooks for this event type
|
break # Ignore, no webhooks for this event type
|
||||||
|
|
||||||
|
if _org != logentry.organizer or _at != logentry.action_type or webhooks is None:
|
||||||
|
_org = logentry.organizer
|
||||||
|
_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(
|
webhooks = WebHook.objects.annotate(has_el=Exists(event_listener)).filter(
|
||||||
organizer=logentry.organizer,
|
organizer=logentry.organizer,
|
||||||
has_el=True,
|
has_el=True,
|
||||||
@@ -206,7 +292,7 @@ def notify_webhooks(logentry_id: int):
|
|||||||
)
|
)
|
||||||
|
|
||||||
for wh in webhooks:
|
for wh in webhooks:
|
||||||
send_webhook.apply_async(args=(logentry_id, notification_type.action_type, wh.pk))
|
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)
|
||||||
@@ -250,7 +336,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))
|
raise self.retry(countdown=2 ** (self.request.retries * 2)) # max is 2 ** (8*2) = 65536 seconds = ~18 hours
|
||||||
except RequestException as e:
|
except RequestException as e:
|
||||||
WebHookCall.objects.create(
|
WebHookCall.objects.create(
|
||||||
webhook=webhook,
|
webhook=webhook,
|
||||||
@@ -262,6 +348,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))
|
raise self.retry(countdown=2 ** (self.request.retries * 2)) # max is 2 ** (8*2) = 65536 seconds = ~18 hours
|
||||||
except MaxRetriesExceededError:
|
except MaxRetriesExceededError:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -73,8 +73,8 @@ banlist = [
|
|||||||
"wtf"
|
"wtf"
|
||||||
]
|
]
|
||||||
|
|
||||||
blacklist_regex = re.compile('(' + '|'.join(banlist) + ')')
|
banlist_regex = re.compile('(' + '|'.join(banlist) + ')')
|
||||||
|
|
||||||
|
|
||||||
def banned(string):
|
def banned(string):
|
||||||
return bool(blacklist_regex.search(string.lower()))
|
return bool(banlist_regex.search(string.lower()))
|
||||||
|
|||||||
@@ -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=False
|
required=True
|
||||||
)),
|
)),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -53,9 +53,23 @@ 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
|
||||||
@@ -150,6 +164,10 @@ 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
|
||||||
|
|
||||||
@@ -163,6 +181,23 @@ 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(
|
||||||
@@ -234,6 +269,14 @@ 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):
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ 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 EU_COUNTRIES, cc_to_vat_prefix
|
from pretix.base.models.tax import (
|
||||||
|
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,
|
||||||
@@ -648,7 +650,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 cc not in EU_COUNTRIES and fprefix + 'vat_id' in self.data:
|
if cc and not is_eu_country(cc) 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']
|
||||||
|
|
||||||
@@ -698,7 +700,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 data.get('country') in EU_COUNTRIES:
|
if data.get('is_business') and not is_eu_country(data.get('country')):
|
||||||
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'):
|
||||||
@@ -722,7 +724,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 data.get('country') in EU_COUNTRIES and data.get('vat_id'):
|
elif self.validate_vat_id and data.get('is_business') and is_eu_country(data.get('country')) 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:
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ from urllib.parse import urlsplit
|
|||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import Http404, 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
|
||||||
@@ -252,3 +253,15 @@ 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
|
||||||
|
|||||||
20
src/pretix/base/migrations/0170_remove_hidden_urls.py
Normal file
20
src/pretix/base/migrations/0170_remove_hidden_urls.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# 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)
|
||||||
|
]
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 3.0.11 on 2020-12-22 10:28
|
# Generated by Django 3.0.11 on 2020-12-22 10:30
|
||||||
|
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ from django.db import migrations
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('pretixbase', '0169_checkinlist_gates'),
|
('pretixbase', '0170_remove_hidden_urls'),
|
||||||
('pretixbase', '0162b_auto_20201218_1810'),
|
('pretixbase', '0162b_auto_20201218_1810'),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -51,9 +51,8 @@ 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 get_all_webhook_events, notify_webhooks
|
from pretix.api.webhooks import 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
|
||||||
@@ -95,21 +94,11 @@ class LoggingMixin:
|
|||||||
if save:
|
if save:
|
||||||
logentry.save()
|
logentry.save()
|
||||||
|
|
||||||
no_types = get_all_notification_types()
|
if logentry.notification_type:
|
||||||
wh_types = get_all_webhook_events()
|
|
||||||
|
|
||||||
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,))
|
notify.apply_async(args=(logentry.pk,))
|
||||||
if wh_type:
|
if logentry.webhook_type:
|
||||||
notify_webhooks.apply_async(args=(logentry.pk,))
|
notify_webhooks.apply_async(args=(logentry.pk,))
|
||||||
|
|
||||||
return logentry
|
return logentry
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -222,3 +222,15 @@ 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()
|
||||||
|
|||||||
@@ -118,25 +118,49 @@ 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.presale_end:
|
if self.effective_presale_end:
|
||||||
return now() > self.presale_end
|
return now() > self.effective_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.presale_start and now() < self.presale_start:
|
if self.effective_presale_start and now() < self.effective_presale_start:
|
||||||
return False
|
return False
|
||||||
return not self.presale_has_ended
|
return not self.presale_has_ended
|
||||||
|
|
||||||
@@ -244,6 +268,34 @@ 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):
|
||||||
@@ -394,7 +446,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 free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False):
|
def _seats(self, ignore_voucher=None):
|
||||||
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,
|
||||||
@@ -402,13 +454,7 @@ 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)
|
||||||
|
|
||||||
qs = qs_annotated.filter(has_order=False, has_cart=False, has_voucher=False)
|
return qs_annotated
|
||||||
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):
|
||||||
@@ -507,11 +553,14 @@ 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, ItemCategory, ItemMetaValue, Question, Quota,
|
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, Question,
|
||||||
|
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})
|
||||||
@@ -573,6 +622,14 @@ 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())
|
||||||
@@ -1089,19 +1146,13 @@ 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 free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False):
|
def _seats(self, ignore_voucher=None):
|
||||||
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)
|
||||||
qs = qs_annotated.filter(has_order=False, has_cart=False, has_voucher=False)
|
return qs_annotated
|
||||||
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):
|
||||||
|
|||||||
@@ -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 of waiting lists are enabled for this event."),
|
help_text=_("This will only work if waiting lists are enabled for this event."),
|
||||||
default=True
|
default=True
|
||||||
)
|
)
|
||||||
show_quota_left = models.NullBooleanField(
|
show_quota_left = models.NullBooleanField(
|
||||||
|
|||||||
@@ -63,14 +63,42 @@ 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
|
||||||
@@ -188,3 +216,16 @@ 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,))
|
||||||
|
|||||||
@@ -1600,6 +1600,10 @@ 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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -357,3 +357,15 @@ 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()
|
||||||
|
|||||||
@@ -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.CASCADE)
|
product = models.ForeignKey('Item', null=True, blank=True, related_name='seats', on_delete=models.SET_NULL)
|
||||||
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)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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 _
|
||||||
from i18nfield.fields import I18nCharField
|
from i18nfield.fields import I18nCharField
|
||||||
|
|
||||||
@@ -85,6 +86,14 @@ 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'
|
||||||
@@ -127,6 +136,9 @@ 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
|
||||||
|
|
||||||
@@ -169,6 +181,8 @@ 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)
|
||||||
@@ -241,7 +255,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 str(invoice_address.country) not in EU_COUNTRIES:
|
if r['country'] == 'EU' and not is_eu_country(invoice_address.country):
|
||||||
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
|
||||||
@@ -265,7 +279,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 str(invoice_address.country) not in EU_COUNTRIES:
|
if not is_eu_country(invoice_address.country):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if invoice_address.country == self.home_country:
|
if invoice_address.country == self.home_country:
|
||||||
@@ -279,6 +293,8 @@ 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:
|
||||||
@@ -289,7 +305,7 @@ class TaxRule(LoggedModel):
|
|||||||
# No country specified? Always apply VAT!
|
# No country specified? Always apply VAT!
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if str(invoice_address.country) not in EU_COUNTRIES:
|
if not is_eu_country(invoice_address.country):
|
||||||
# Non-EU country? Never apply VAT!
|
# Non-EU country? Never apply VAT!
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
@@ -513,7 +513,7 @@ class BasePaymentProvider:
|
|||||||
|
|
||||||
return timing and pricing
|
return timing and pricing
|
||||||
|
|
||||||
def payment_form_render(self, request: HttpRequest, total: Decimal) -> str:
|
def payment_form_render(self, request: HttpRequest, total: Decimal, order: Order=None) -> 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,13 +522,15 @@ 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) -> str:
|
def checkout_confirm_render(self, request, order: Order=None) -> 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.
|
||||||
@@ -537,6 +539,8 @@ 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
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
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
|
||||||
@@ -52,10 +53,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,
|
||||||
current_secret: str = None, force_invalidate=False) -> str:
|
attendee_name: str = None, 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``,
|
||||||
and the current secret ``current_secret`` (if any).
|
attendee name ``attendee_name`` (can be ``None``) 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+/=``.
|
||||||
|
|
||||||
@@ -70,6 +71,11 @@ 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()
|
||||||
|
|
||||||
@@ -80,7 +86,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,
|
||||||
current_secret: str = None, force_invalidate=False):
|
attendee_name: str = None, 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(
|
||||||
@@ -187,12 +193,17 @@ 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:
|
||||||
|
|||||||
@@ -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=subevent or order.event,
|
email_context = get_email_context(event_or_subevent=p.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,11 +82,12 @@ 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, keep_fee_fixed: str,
|
def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
|
||||||
keep_fee_percentage: str, keep_fees: list=None, manual_refund: bool=False,
|
keep_fee_fixed: str, keep_fee_per_ticket: str, keep_fee_percentage: str, keep_fees: list=None,
|
||||||
send: bool=False, send_subject: dict=None, send_message: dict=None,
|
manual_refund: bool=False, 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)
|
||||||
@@ -102,14 +103,20 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_
|
|||||||
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=subevent
|
subevent__in=subevents
|
||||||
)
|
)
|
||||||
has_other_subevent = OrderPosition.objects.filter(order_id=OuterRef('pk')).exclude(
|
has_other_subevent = OrderPosition.objects.filter(order_id=OuterRef('pk')).exclude(
|
||||||
subevent=subevent
|
subevent__in=subevents
|
||||||
)
|
)
|
||||||
orders_to_change = orders_to_cancel.annotate(
|
orders_to_change = orders_to_cancel.annotate(
|
||||||
has_subevent=Exists(has_subevent),
|
has_subevent=Exists(has_subevent),
|
||||||
@@ -124,15 +131,18 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_
|
|||||||
has_subevent=True, has_other_subevent=False
|
has_subevent=True, has_other_subevent=False
|
||||||
)
|
)
|
||||||
|
|
||||||
subevent.log_action(
|
for se in subevents:
|
||||||
|
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,
|
||||||
@@ -146,7 +156,9 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_
|
|||||||
)
|
)
|
||||||
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(subevent=subevent, voucher__isnull=True)
|
qs_wl = event.waitinglistentries.filter(voucher__isnull=True).select_related('subevent')
|
||||||
|
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
|
||||||
@@ -170,6 +182,10 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_
|
|||||||
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)
|
||||||
@@ -201,16 +217,20 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_
|
|||||||
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 == subevent:
|
if p.subevent_id in subevent_ids:
|
||||||
total += p.price
|
total += p.price
|
||||||
ocm.cancel(p)
|
ocm.cancel(p)
|
||||||
positions.append(p)
|
positions.append(p)
|
||||||
|
|
||||||
fee = Decimal('0.00')
|
if keep_fee_per_ticket:
|
||||||
|
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:
|
||||||
@@ -246,7 +266,7 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_
|
|||||||
|
|
||||||
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, subevent)
|
_send_wle_mail(wle, send_waitinglist_subject, send_waitinglist_message, wle.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:
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ 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.'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -324,6 +325,8 @@ 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'])
|
||||||
@@ -1063,6 +1066,7 @@ 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
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
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, Event, Organizer, User, cachedfile_name,
|
CachedFile, Device, Event, Organizer, TeamAPIToken, User, cachedfile_name,
|
||||||
)
|
)
|
||||||
from pretix.base.services.tasks import (
|
from pretix.base.services.tasks import (
|
||||||
ProfiledEventTask, ProfiledOrganizerUserTask,
|
ProfiledEventTask, ProfiledOrganizerUserTask,
|
||||||
@@ -48,7 +49,13 @@ 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, fileid: str, provider: str, form_data: Dict[str, Any]) -> None:
|
def multiexport(self, organizer: Organizer, user: User, device: int, token: int, 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(
|
||||||
@@ -57,9 +64,21 @@ def multiexport(self, organizer: Organizer, user: User, fileid: str, provider: s
|
|||||||
)
|
)
|
||||||
|
|
||||||
file = CachedFile.objects.get(id=fileid)
|
file = CachedFile.objects.get(id=fileid)
|
||||||
with language(user.locale), override(user.timezone):
|
if user:
|
||||||
allowed_events = user.get_events_with_permission('can_view_orders')
|
locale = user.locale
|
||||||
|
timezone = user.timezone
|
||||||
|
else:
|
||||||
|
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'))
|
events = allowed_events.filter(pk__in=form_data.get('events'))
|
||||||
responses = register_multievent_data_exporters.send(organizer)
|
responses = register_multievent_data_exporters.send(organizer)
|
||||||
|
|
||||||
|
|||||||
@@ -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_COUNTRIES, EU_CURRENCIES
|
from pretix.base.models.tax import EU_CURRENCIES, is_eu_country
|
||||||
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
|
||||||
@@ -181,7 +181,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
|||||||
if reverse_charge:
|
if reverse_charge:
|
||||||
if invoice.additional_text:
|
if invoice.additional_text:
|
||||||
invoice.additional_text += "<br /><br />"
|
invoice.additional_text += "<br /><br />"
|
||||||
if str(invoice.invoice_to_country) in EU_COUNTRIES:
|
if is_eu_country(invoice.invoice_to_country):
|
||||||
invoice.additional_text += pgettext(
|
invoice.additional_text += pgettext(
|
||||||
"invoice",
|
"invoice",
|
||||||
"Reverse Charge: According to Article 194, 196 of Council Directive 2006/112/EEC, VAT liability "
|
"Reverse Charge: According to Article 194, 196 of Council Directive 2006/112/EEC, VAT liability "
|
||||||
|
|||||||
@@ -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 * 2))
|
self.retry(max_retries=5, countdown=2 ** (self.request.retries * 3)) # max is 2 ** (4*3) = 4096 seconds = 68 minutes
|
||||||
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 * 2))
|
self.retry(max_retries=5, countdown=2 ** (self.request.retries * 3)) # max is 2 ** (4*3) = 4096 seconds = 68 minutes
|
||||||
if order:
|
if order:
|
||||||
order.log_action(
|
order.log_action(
|
||||||
'pretix.event.order.email.error',
|
'pretix.event.order.email.error',
|
||||||
|
|||||||
@@ -15,21 +15,25 @@ 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_id: int):
|
def notify(logentry_ids: list):
|
||||||
logentry = LogEntry.all.select_related('event', 'event__organizer').get(id=logentry_id)
|
if not isinstance(logentry_ids, list):
|
||||||
if not logentry.event:
|
logentry_ids = [logentry_ids]
|
||||||
return # Ignore, we only have event-related notifications right now
|
|
||||||
types = get_all_notification_types(logentry.event)
|
|
||||||
|
|
||||||
notification_type = None
|
qs = LogEntry.all.select_related('event', 'event__organizer').filter(id__in=logentry_ids)
|
||||||
typepath = logentry.action_type
|
|
||||||
while not notification_type and '.' in typepath:
|
_event, _at, notify_specific, notify_global = None, None, None, None
|
||||||
notification_type = types.get(typepath + ('.*' if typepath != logentry.action_type else ''))
|
for logentry in qs:
|
||||||
typepath = typepath.rsplit('.', 1)[0]
|
if not logentry.event:
|
||||||
|
break # Ignore, we only have event-related notifications right now
|
||||||
|
|
||||||
|
notification_type = logentry.notification_type
|
||||||
|
|
||||||
if not notification_type:
|
if not notification_type:
|
||||||
return # No suitable plugin
|
break # No suitable plugin
|
||||||
|
|
||||||
|
if _event != logentry.event or _at != logentry.action_type or notify_global is None:
|
||||||
|
_event = logentry.event
|
||||||
|
_at = logentry.action_type
|
||||||
# All users that have the permission to get the notification
|
# All users that have the permission to get the notification
|
||||||
users = logentry.event.get_users_with_permission(
|
users = logentry.event.get_users_with_permission(
|
||||||
notification_type.required_permission
|
notification_type.required_permission
|
||||||
@@ -58,12 +62,12 @@ def notify(logentry_id: int):
|
|||||||
for um, enabled in notify_specific.items():
|
for um, enabled in notify_specific.items():
|
||||||
user, method = um
|
user, method = um
|
||||||
if enabled:
|
if enabled:
|
||||||
send_notification.apply_async(args=(logentry_id, notification_type.action_type, user.pk, method))
|
send_notification.apply_async(args=(logentry.id, notification_type.action_type, user.pk, method))
|
||||||
|
|
||||||
for um, enabled in notify_global.items():
|
for um, enabled in notify_global.items():
|
||||||
user, method = um
|
user, method = um
|
||||||
if enabled and um not in notify_specific:
|
if enabled and um not in notify_specific:
|
||||||
send_notification.apply_async(args=(logentry_id, notification_type.action_type, user.pk, method))
|
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)
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ 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__)
|
||||||
@@ -615,6 +616,7 @@ 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)
|
||||||
@@ -643,6 +645,10 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
|||||||
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)
|
||||||
@@ -1174,6 +1180,7 @@ 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'))
|
||||||
@@ -1241,8 +1248,11 @@ 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'])
|
||||||
@@ -1262,8 +1272,11 @@ 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'])
|
||||||
@@ -1321,7 +1334,10 @@ 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':
|
||||||
@@ -1374,10 +1390,13 @@ 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'])
|
||||||
@@ -1952,7 +1971,10 @@ 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()
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ 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')
|
||||||
)
|
)
|
||||||
@@ -135,6 +136,7 @@ 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,
|
||||||
@@ -198,6 +200,7 @@ 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')
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ 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
|
||||||
|
|
||||||
|
|||||||
@@ -2049,7 +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': 'title_salutation_given_family',
|
'_scheme': 'salutation_title_given_family',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -4,3 +4,4 @@
|
|||||||
<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 %}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ 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
|
||||||
@@ -77,6 +78,8 @@ 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
|
||||||
@@ -84,6 +87,8 @@ 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
|
||||||
@@ -93,6 +98,48 @@ 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
|
||||||
|
|
||||||
|
|
||||||
@@ -129,7 +176,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 data:
|
if isinstance(data, File):
|
||||||
filename = data.name
|
filename = data.name
|
||||||
ext = os.path.splitext(filename)[1]
|
ext = os.path.splitext(filename)[1]
|
||||||
ext = ext.lower()
|
ext = ext.lower()
|
||||||
@@ -138,6 +185,51 @@ 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(),
|
||||||
|
web_download=True,
|
||||||
|
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),
|
||||||
|
web_download=True,
|
||||||
|
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 = ''
|
||||||
|
|||||||
@@ -657,6 +657,8 @@ 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:
|
||||||
@@ -1146,6 +1148,7 @@ class TaxRuleLineForm(forms.Form):
|
|||||||
('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(
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
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.db.models import Exists, F, OuterRef, Q
|
from django.conf import settings
|
||||||
|
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_lazy as _, pgettext_lazy
|
from django.utils.translation import gettext, gettext_lazy as _, pgettext_lazy
|
||||||
|
|
||||||
from pretix.base.forms.widgets import DatePickerWidget
|
from pretix.base.channels import get_all_sales_channels
|
||||||
|
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,
|
||||||
@@ -19,7 +25,9 @@ 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 = []
|
||||||
@@ -83,6 +91,38 @@ 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(
|
||||||
@@ -104,20 +144,29 @@ 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')),
|
)),
|
||||||
(Order.STATUS_PENDING + Order.STATUS_EXPIRED, _('Pending or expired')),
|
(_('Cancellations'), (
|
||||||
(Order.STATUS_CANCELED, _('Canceled')),
|
(Order.STATUS_CANCELED, _('Canceled')),
|
||||||
('cp', _('Canceled (or with paid fee)')),
|
('cp', _('Canceled (or with paid fee)')),
|
||||||
('pa', _('Approval pending')),
|
('rc', _('Cancellation requested')),
|
||||||
|
)),
|
||||||
|
(_('Payment process'), (
|
||||||
|
(Order.STATUS_EXPIRED, _('Expired')),
|
||||||
|
(Order.STATUS_PENDING + Order.STATUS_EXPIRED, _('Pending or expired')),
|
||||||
|
('o', _('Pending (overdue)')),
|
||||||
('overpaid', _('Overpaid')),
|
('overpaid', _('Overpaid')),
|
||||||
('underpaid', _('Underpaid')),
|
('underpaid', _('Underpaid')),
|
||||||
('pendingpaid', _('Pending (but fully paid)')),
|
('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,
|
||||||
)
|
)
|
||||||
@@ -207,6 +256,11 @@ 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
|
||||||
@@ -337,6 +391,238 @@ 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__gte=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',
|
||||||
|
|||||||
@@ -226,6 +226,8 @@ 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()
|
||||||
|
|||||||
@@ -400,7 +400,6 @@ 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']
|
||||||
@@ -517,6 +516,20 @@ 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,
|
||||||
@@ -572,7 +585,21 @@ 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'),
|
||||||
@@ -613,6 +640,12 @@ 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,
|
||||||
@@ -717,6 +750,7 @@ 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,
|
||||||
@@ -733,6 +767,12 @@ class EventCancelForm(forms.Form):
|
|||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
d = super().clean()
|
d = super().clean()
|
||||||
if self.event.has_subevents and not d['subevent'] and not d['all_subevents']:
|
if d.get('subevent') and d.get('subevents_from'):
|
||||||
|
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
|
||||||
|
|||||||
@@ -397,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 settings have been changed.'),
|
'pretix.event.changed': _('The event details 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.'),
|
||||||
|
|||||||
@@ -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') or "event.order." in url.url_name,
|
'active': url.url_name in ('event.orders', 'event.order', 'event.orders.search') or "event.order." in url.url_name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': _('Overview'),
|
'label': _('Overview'),
|
||||||
|
|||||||
@@ -323,3 +323,19 @@ 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.
|
||||||
|
"""
|
||||||
|
|||||||
@@ -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">Event type</label>
|
<label class="col-md-3 control-label">{% trans "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 }}">
|
<input type="radio" value="" name="{{ form.has_subevents.html_name }}" {% if not form.has_subevents.value %}checked{% endif %}>
|
||||||
<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 }}">
|
<input type="radio" value="on" name="{{ form.has_subevents.html_name }}" {% if not form.has_subevents.value %}checked{% endif %}>
|
||||||
<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">
|
||||||
|
|||||||
@@ -14,9 +14,84 @@
|
|||||||
{% 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>
|
||||||
|
|||||||
@@ -10,13 +10,56 @@
|
|||||||
<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" %}
|
|
||||||
{% bootstrap_field form.admission 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-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" %}
|
||||||
|
|||||||
@@ -73,7 +73,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if i.var_count %}
|
{% if i.var_count %}
|
||||||
<span class="fa fa-list-ul fa-fw text-muted" data-toggle="tooltip" title="{% trans "Product with variations" %}"></span>
|
<span class="fa fa-th-large fa-fw text-muted" data-toggle="tooltip" title="{% trans "Product with variations" %}"></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -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 %}
|
||||||
<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">
|
||||||
{% if order.changable and 'can_change_orders' in request.eventpermset %}
|
|
||||||
<a href="{% url "control:event.order.info" 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>
|
<span class="fa fa-edit"></span>
|
||||||
{% trans "Change answers" %}
|
{% trans "Change answers" %}
|
||||||
</a> ·
|
</a>
|
||||||
<a href="{% url "control:event.order.change" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
|
{% if order.changable and 'can_change_orders' in request.eventpermset %}
|
||||||
|
· <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>
|
||||||
@@ -390,6 +390,10 @@
|
|||||||
<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>
|
||||||
|
|||||||
@@ -25,6 +25,13 @@
|
|||||||
<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>
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
Since the order is already canceled, this will not affect its state.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
<p>
|
<p>
|
||||||
{% blocktrans trimmed with amount=refund.amount|money:request.event.currency method=refund.payment_provider.verbose_name %}
|
{% blocktrans trimmed with amount=refund.amount|money:request.event.currency method=refund.payment_provider.verbose_name %}
|
||||||
What should happen to the ticket order?
|
What should happen to the ticket order?
|
||||||
@@ -41,6 +48,7 @@
|
|||||||
{% trans "Cancel the order irrevocably." %}
|
{% trans "Cancel the order irrevocably." %}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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 %}">
|
||||||
|
|||||||
@@ -27,8 +27,10 @@
|
|||||||
{% 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.subevent layout="control" %}
|
|
||||||
{% bootstrap_field form.all_subevents layout="control" %}
|
{% bootstrap_field form.all_subevents layout="control" %}
|
||||||
|
{% bootstrap_field form.subevent layout="control" %}
|
||||||
|
{% bootstrap_field form.subevents_from layout="control" %}
|
||||||
|
{% bootstrap_field form.subevents_to layout="control" %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
@@ -39,6 +41,7 @@
|
|||||||
{% 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>
|
||||||
|
|||||||
@@ -2,19 +2,36 @@
|
|||||||
{% 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 }}">{% trans "Approval pending" %}</span>
|
<span class="label label-warning {{ class }}">
|
||||||
|
<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 }}">{% trans "Pending" %}</span>
|
class="label label-warning {{ class }}">
|
||||||
|
<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 }}">{% trans "Canceled (paid fee)" %}</span>
|
<span class="label label-info {{ class }}">
|
||||||
|
<span class="fa fa-times"></span>
|
||||||
|
{% trans "Canceled (paid fee)" %}
|
||||||
|
</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="label label-success {{ class }}">{% trans "Paid" %}</span>
|
<span class="label label-success {{ class }}">
|
||||||
|
<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 }}">{% trans "Expired" %}</span>
|
<span class="label label-danger {{ class }}">
|
||||||
|
<span class="fa fa-clock-o"></span>
|
||||||
|
{% trans "Expired" %}</span>
|
||||||
{% elif order.status == "c" %}
|
{% elif order.status == "c" %}
|
||||||
<span class="label label-danger {{ class }}">{% trans "Canceled" %}</span>
|
<span class="label label-danger {{ class }}">
|
||||||
|
<span class="fa fa-times"></span>
|
||||||
|
{% trans "Canceled" %}
|
||||||
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -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 %}
|
{% if not filter_form.filtered and orders|length == 0 and not filter_strings %}
|
||||||
<div class="empty-collection">
|
<div class="empty-collection">
|
||||||
<p>
|
<p>
|
||||||
{% blocktrans trimmed %}
|
{% blocktrans trimmed %}
|
||||||
@@ -21,11 +21,23 @@
|
|||||||
{% 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">
|
<a href="{% eventurl request.event "presale:event.index" %}" class="btn btn-primary btn-lg" target="_blank">
|
||||||
{% trans "Go to the ticket shop" %}
|
{% trans "Go to the ticket shop" %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% else %}
|
||||||
|
{% if filter_strings %}
|
||||||
|
<p>
|
||||||
|
<span class="fa fa-filter"></span>
|
||||||
|
{% trans "Search query:" %}
|
||||||
|
{{ filter_strings|join:" · " }}
|
||||||
|
·
|
||||||
|
<a href="{% url "control:event.orders.search" event=request.event.slug organizer=request.event.organizer.slug %}?{{ request.META.QUERY_STRING }}">
|
||||||
|
<span class="fa fa-edit"></span>
|
||||||
|
{% trans "Edit" %}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="row filter-form">
|
<div class="row filter-form">
|
||||||
<form class="col-md-2 col-xs-12"
|
<form class="col-md-2 col-xs-12"
|
||||||
@@ -62,16 +74,19 @@
|
|||||||
<div class="col-md-2 col-xs-6">
|
<div class="col-md-2 col-xs-6">
|
||||||
{% bootstrap_field filter_form.query layout='inline' %}
|
{% bootstrap_field filter_form.query layout='inline' %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2 col-xs-6">
|
<div class="col-md-1 col-xs-6">
|
||||||
<button class="btn btn-primary btn-block" type="submit">
|
<button class="btn btn-primary btn-block" type="submit">
|
||||||
<span class="fa fa-filter"></span>
|
<span class="fa fa-filter"></span>
|
||||||
<span class="hidden-md">
|
|
||||||
{% trans "Filter" %}
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
</form>
|
||||||
</div>
|
</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>
|
||||||
|
|||||||
@@ -12,7 +12,14 @@
|
|||||||
<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>{% trans "Order overview" %}</h1>
|
<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 %}
|
||||||
@@ -58,12 +65,14 @@
|
|||||||
<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>
|
||||||
@@ -76,6 +85,7 @@
|
|||||||
<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>
|
||||||
@@ -95,7 +105,12 @@
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ listurl }}?item={{ item.id }}&status=n&provider={{ item.provider }}">
|
<a href="{{ listurl }}?item={{ item.id }}&status=pa&provider={{ item.provider }}">
|
||||||
|
{{ item.num.unapproved|togglesum:request.event.currency }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ listurl }}?item={{ item.id }}&status=na&provider={{ item.provider }}">
|
||||||
{{ item.num.pending|togglesum:request.event.currency }}
|
{{ item.num.pending|togglesum:request.event.currency }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
@@ -123,7 +138,12 @@
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ listurl }}?item={{ item.id }}-{{ var.id }}&status=n&provider={{ item.provider }}">
|
<a href="{{ listurl }}?item={{ item.id }}-{{ var.id }}&status=pa&provider={{ item.provider }}">
|
||||||
|
{{ var.num.unapproved|togglesum:request.event.currency }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ listurl }}?item={{ item.id }}-{{ var.id }}&status=na&provider={{ item.provider }}">
|
||||||
{{ var.num.pending|togglesum:request.event.currency }}
|
{{ var.num.pending|togglesum:request.event.currency }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
@@ -146,6 +166,7 @@
|
|||||||
<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>
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{% 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 %}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{% extends "pretixcontrol/auth/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
{% block title %}{% trans "Disable notifications" %}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<form action="" method="post" class="form-signin">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="text-center">
|
||||||
|
<p>Please confirm that you no longer want to receive notifications for any of your events.</p>
|
||||||
|
<p><button type="submit" class="btn btn-primary" value="Disable notifications">Disable notifications</button></p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
@@ -253,6 +253,8 @@ urlpatterns = [
|
|||||||
name='event.order.info'),
|
name='event.order.info'),
|
||||||
url(r'^orders/(?P<code>[0-9A-Z]+)/sendmail$', orders.OrderSendMail.as_view(),
|
url(r'^orders/(?P<code>[0-9A-Z]+)/sendmail$', orders.OrderSendMail.as_view(),
|
||||||
name='event.order.sendmail'),
|
name='event.order.sendmail'),
|
||||||
|
url(r'^orders/(?P<code>[0-9A-Z]+)/(?P<position>[0-9A-Z]+)/sendmail$', orders.OrderPositionSendMail.as_view(),
|
||||||
|
name='event.order.position.sendmail'),
|
||||||
url(r'^orders/(?P<code>[0-9A-Z]+)/mail_history$', orders.OrderEmailHistory.as_view(),
|
url(r'^orders/(?P<code>[0-9A-Z]+)/mail_history$', orders.OrderEmailHistory.as_view(),
|
||||||
name='event.order.mail_history'),
|
name='event.order.mail_history'),
|
||||||
url(r'^orders/(?P<code>[0-9A-Z]+)/payments/(?P<payment>\d+)/cancel$', orders.OrderPaymentCancel.as_view(),
|
url(r'^orders/(?P<code>[0-9A-Z]+)/payments/(?P<payment>\d+)/cancel$', orders.OrderPaymentCancel.as_view(),
|
||||||
@@ -281,6 +283,7 @@ urlpatterns = [
|
|||||||
url(r'^orders/refunds/$', orders.RefundList.as_view(), name='event.orders.refunds'),
|
url(r'^orders/refunds/$', orders.RefundList.as_view(), name='event.orders.refunds'),
|
||||||
url(r'^orders/go$', orders.OrderGo.as_view(), name='event.orders.go'),
|
url(r'^orders/go$', orders.OrderGo.as_view(), name='event.orders.go'),
|
||||||
url(r'^orders/$', orders.OrderList.as_view(), name='event.orders'),
|
url(r'^orders/$', orders.OrderList.as_view(), name='event.orders'),
|
||||||
|
url(r'^orders/search$', orders.OrderSearch.as_view(), name='event.orders.search'),
|
||||||
url(r'^dangerzone/$', event.DangerZone.as_view(), name='event.dangerzone'),
|
url(r'^dangerzone/$', event.DangerZone.as_view(), name='event.dangerzone'),
|
||||||
url(r'^cancel/$', orders.EventCancel.as_view(), name='event.cancel'),
|
url(r'^cancel/$', orders.EventCancel.as_view(), name='event.cancel'),
|
||||||
url(r'^shredder/$', shredder.StartShredView.as_view(), name='event.shredder.start'),
|
url(r'^shredder/$', shredder.StartShredView.as_view(), name='event.shredder.start'),
|
||||||
|
|||||||
@@ -937,6 +937,7 @@ class EventDelete(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixi
|
|||||||
data={
|
data={
|
||||||
'event_id': self.request.event.pk,
|
'event_id': self.request.event.pk,
|
||||||
'name': str(self.request.event.name),
|
'name': str(self.request.event.name),
|
||||||
|
'slug': self.request.event.slug,
|
||||||
'logentries': list(self.request.event.logentry_set.values_list('pk', flat=True))
|
'logentries': list(self.request.event.logentry_set.values_list('pk', flat=True))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -236,6 +236,10 @@ class EventWizard(SafeSessionWizardView):
|
|||||||
event.has_subevents = foundation_data['has_subevents']
|
event.has_subevents = foundation_data['has_subevents']
|
||||||
event.testmode = True
|
event.testmode = True
|
||||||
form_dict['basics'].save()
|
form_dict['basics'].save()
|
||||||
|
event.log_action(
|
||||||
|
'pretix.event.added',
|
||||||
|
user=self.request.user,
|
||||||
|
)
|
||||||
|
|
||||||
if not EventWizardBasicsForm.has_control_rights(self.request.user, event.organizer):
|
if not EventWizardBasicsForm.has_control_rights(self.request.user, event.organizer):
|
||||||
if basics_data["team"] is not None:
|
if basics_data["team"] is not None:
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ from pretix.base.models import (
|
|||||||
from pretix.base.models.orders import (
|
from pretix.base.models.orders import (
|
||||||
CancellationRequest, OrderFee, OrderPayment, OrderPosition, OrderRefund,
|
CancellationRequest, OrderFee, OrderPayment, OrderPosition, OrderRefund,
|
||||||
)
|
)
|
||||||
from pretix.base.models.tax import EU_COUNTRIES, cc_to_vat_prefix
|
from pretix.base.models.tax import cc_to_vat_prefix, is_eu_country
|
||||||
from pretix.base.payment import PaymentException
|
from pretix.base.payment import PaymentException
|
||||||
from pretix.base.secrets import assign_ticket_secret
|
from pretix.base.secrets import assign_ticket_secret
|
||||||
from pretix.base.services import tickets
|
from pretix.base.services import tickets
|
||||||
@@ -74,16 +74,18 @@ from pretix.base.templatetags.rich_text import markdown_compile_email
|
|||||||
from pretix.base.views.mixins import OrderQuestionsViewMixin
|
from pretix.base.views.mixins import OrderQuestionsViewMixin
|
||||||
from pretix.base.views.tasks import AsyncAction
|
from pretix.base.views.tasks import AsyncAction
|
||||||
from pretix.control.forms.filter import (
|
from pretix.control.forms.filter import (
|
||||||
EventOrderFilterForm, OverviewFilterForm, RefundFilterForm,
|
EventOrderExpertFilterForm, EventOrderFilterForm, OverviewFilterForm,
|
||||||
|
RefundFilterForm,
|
||||||
)
|
)
|
||||||
from pretix.control.forms.orders import (
|
from pretix.control.forms.orders import (
|
||||||
CancelForm, CommentForm, ConfirmPaymentForm, EventCancelForm, ExporterForm,
|
CancelForm, CommentForm, ConfirmPaymentForm, EventCancelForm, ExporterForm,
|
||||||
ExtendForm, MarkPaidForm, OrderContactForm, OrderFeeChangeForm,
|
ExtendForm, MarkPaidForm, OrderContactForm, OrderFeeChangeForm,
|
||||||
OrderLocaleForm, OrderMailForm, OrderPositionAddForm,
|
OrderLocaleForm, OrderMailForm, OrderPositionAddForm,
|
||||||
OrderPositionAddFormset, OrderPositionChangeForm, OrderRefundForm,
|
OrderPositionAddFormset, OrderPositionChangeForm, OrderPositionMailForm,
|
||||||
OtherOperationsForm,
|
OrderRefundForm, OtherOperationsForm,
|
||||||
)
|
)
|
||||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||||
|
from pretix.control.signals import order_search_forms
|
||||||
from pretix.control.views import PaginationMixin
|
from pretix.control.views import PaginationMixin
|
||||||
from pretix.helpers.safedownload import check_token
|
from pretix.helpers.safedownload import check_token
|
||||||
from pretix.presale.signals import question_form_fields
|
from pretix.presale.signals import question_form_fields
|
||||||
@@ -91,7 +93,31 @@ from pretix.presale.signals import question_form_fields
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class OrderList(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
class OrderSearchMixin:
|
||||||
|
def get_forms(self):
|
||||||
|
f = [
|
||||||
|
EventOrderExpertFilterForm(
|
||||||
|
data=self.request.GET,
|
||||||
|
event=self.request.event,
|
||||||
|
prefix='expert',
|
||||||
|
)
|
||||||
|
]
|
||||||
|
for recv, resp in order_search_forms.send(sender=self.request.event, request=self.request):
|
||||||
|
f.append(resp)
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
class OrderSearch(OrderSearchMixin, EventPermissionRequiredMixin, TemplateView):
|
||||||
|
template_name = 'pretixcontrol/orders/search.html'
|
||||||
|
permission = 'can_view_orders'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
ctx['forms'] = self.get_forms()
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||||
model = Order
|
model = Order
|
||||||
context_object_name = 'orders'
|
context_object_name = 'orders'
|
||||||
template_name = 'pretixcontrol/orders/index.html'
|
template_name = 'pretixcontrol/orders/index.html'
|
||||||
@@ -105,12 +131,21 @@ class OrderList(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
|||||||
if self.filter_form.is_valid():
|
if self.filter_form.is_valid():
|
||||||
qs = self.filter_form.filter_qs(qs)
|
qs = self.filter_form.filter_qs(qs)
|
||||||
|
|
||||||
|
for f in self.get_forms():
|
||||||
|
if any(k.startswith(f.prefix) for k in self.request.GET.keys()) and f.is_valid():
|
||||||
|
qs = f.filter_qs(qs)
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
ctx = super().get_context_data(**kwargs)
|
ctx = super().get_context_data(**kwargs)
|
||||||
ctx['filter_form'] = self.filter_form
|
ctx['filter_form'] = self.filter_form
|
||||||
|
|
||||||
|
ctx['filter_strings'] = []
|
||||||
|
for f in self.get_forms():
|
||||||
|
if any(k.startswith(f.prefix) for k in self.request.GET.keys()) and f.is_valid():
|
||||||
|
ctx['filter_strings'] += f.filter_to_strings()
|
||||||
|
|
||||||
# Only compute this annotations for this page (query optimization)
|
# Only compute this annotations for this page (query optimization)
|
||||||
s = OrderPosition.objects.filter(
|
s = OrderPosition.objects.filter(
|
||||||
order=OuterRef('pk')
|
order=OuterRef('pk')
|
||||||
@@ -566,7 +601,8 @@ class OrderRefundProcess(OrderView):
|
|||||||
if self.refund.state == OrderRefund.REFUND_STATE_EXTERNAL:
|
if self.refund.state == OrderRefund.REFUND_STATE_EXTERNAL:
|
||||||
self.refund.done(user=self.request.user)
|
self.refund.done(user=self.request.user)
|
||||||
|
|
||||||
if self.request.POST.get("action") == "r" and (self.order.status != Order.STATUS_CANCELED and self.order.positions.exists()):
|
if self.order.status != Order.STATUS_CANCELED and self.order.positions.exists():
|
||||||
|
if self.request.POST.get("action") == "r":
|
||||||
mark_order_refunded(self.order, user=self.request.user)
|
mark_order_refunded(self.order, user=self.request.user)
|
||||||
elif not (self.order.status == Order.STATUS_PAID and self.order.pending_sum <= 0):
|
elif not (self.order.status == Order.STATUS_PAID and self.order.pending_sum <= 0):
|
||||||
self.order.status = Order.STATUS_PENDING
|
self.order.status = Order.STATUS_PENDING
|
||||||
@@ -1130,7 +1166,7 @@ class OrderCheckVATID(OrderView):
|
|||||||
messages.error(self.request, _('No country specified.'))
|
messages.error(self.request, _('No country specified.'))
|
||||||
return redirect(self.get_order_url())
|
return redirect(self.get_order_url())
|
||||||
|
|
||||||
if str(ia.country) not in EU_COUNTRIES:
|
if not is_eu_country(ia.country):
|
||||||
messages.error(self.request, _('VAT ID could not be checked since a non-EU country has been '
|
messages.error(self.request, _('VAT ID could not be checked since a non-EU country has been '
|
||||||
'specified.'))
|
'specified.'))
|
||||||
return redirect(self.get_order_url())
|
return redirect(self.get_order_url())
|
||||||
@@ -1515,7 +1551,7 @@ class OrderChange(OrderView):
|
|||||||
elif change_subevent is not None:
|
elif change_subevent is not None:
|
||||||
ocm.change_subevent(p, *change_subevent)
|
ocm.change_subevent(p, *change_subevent)
|
||||||
|
|
||||||
if p.form.cleaned_data.get('seat') and (not p.seat or p.form.cleaned_data['seat'] != p.seat.seat_guid):
|
if p.form.cleaned_data.get('seat') and (not p.seat or p.form.cleaned_data['seat'] != p.seat.seat_guid or change_subevent):
|
||||||
ocm.change_seat(p, p.form.cleaned_data['seat'])
|
ocm.change_seat(p, p.form.cleaned_data['seat'])
|
||||||
|
|
||||||
if p.form.cleaned_data['price'] is not None and p.form.cleaned_data['price'] != p.price:
|
if p.form.cleaned_data['price'] is not None and p.form.cleaned_data['price'] != p.price:
|
||||||
@@ -1783,6 +1819,57 @@ class OrderSendMail(EventPermissionRequiredMixin, OrderViewMixin, FormView):
|
|||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
class OrderPositionSendMail(OrderSendMail):
|
||||||
|
form_class = OrderPositionMailForm
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
kwargs = super().get_form_kwargs()
|
||||||
|
kwargs['position'] = get_object_or_404(
|
||||||
|
OrderPosition,
|
||||||
|
order__event=self.request.event,
|
||||||
|
order__code=self.kwargs['code'].upper(),
|
||||||
|
pk=self.kwargs['position'],
|
||||||
|
attendee_email__isnull=False
|
||||||
|
)
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
position = get_object_or_404(
|
||||||
|
OrderPosition,
|
||||||
|
order__event=self.request.event,
|
||||||
|
order__code=self.kwargs['code'].upper(),
|
||||||
|
pk=self.kwargs['position'],
|
||||||
|
attendee_email__isnull=False
|
||||||
|
)
|
||||||
|
self.preview_output = {}
|
||||||
|
with language(position.order.locale):
|
||||||
|
email_context = get_email_context(event=position.order.event, order=position.order, position=position)
|
||||||
|
email_template = LazyI18nString(form.cleaned_data['message'])
|
||||||
|
email_subject = str(form.cleaned_data['subject']).format_map(TolerantDict(email_context))
|
||||||
|
email_content = render_mail(email_template, email_context)
|
||||||
|
if self.request.POST.get('action') == 'preview':
|
||||||
|
self.preview_output = {
|
||||||
|
'subject': _('Subject: {subject}').format(subject=email_subject),
|
||||||
|
'html': markdown_compile_email(email_content)
|
||||||
|
}
|
||||||
|
return self.get(self.request, *self.args, **self.kwargs)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
position.send_mail(
|
||||||
|
form.cleaned_data['subject'],
|
||||||
|
email_template,
|
||||||
|
email_context,
|
||||||
|
'pretix.event.order.position.email.custom_sent',
|
||||||
|
self.request.user
|
||||||
|
)
|
||||||
|
messages.success(self.request,
|
||||||
|
_('Your message has been queued and will be sent to {}.'.format(position.attendee_email)))
|
||||||
|
except SendMailException:
|
||||||
|
messages.error(self.request,
|
||||||
|
_('Failed to send mail to the following user: {}'.format(position.attendee_email)))
|
||||||
|
return super(OrderSendMail, self).form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
class OrderEmailHistory(EventPermissionRequiredMixin, OrderViewMixin, ListView):
|
class OrderEmailHistory(EventPermissionRequiredMixin, OrderViewMixin, ListView):
|
||||||
template_name = 'pretixcontrol/order/mail_history.html'
|
template_name = 'pretixcontrol/order/mail_history.html'
|
||||||
permission = 'can_view_orders'
|
permission = 'can_view_orders'
|
||||||
@@ -2023,12 +2110,15 @@ class EventCancel(EventPermissionRequiredMixin, AsyncAction, FormView):
|
|||||||
return self.do(
|
return self.do(
|
||||||
self.request.event.pk,
|
self.request.event.pk,
|
||||||
subevent=form.cleaned_data['subevent'].pk if form.cleaned_data.get('subevent') else None,
|
subevent=form.cleaned_data['subevent'].pk if form.cleaned_data.get('subevent') else None,
|
||||||
|
subevents_from=form.cleaned_data.get('subevents_from'),
|
||||||
|
subevents_to=form.cleaned_data.get('subevents_to'),
|
||||||
auto_refund=form.cleaned_data.get('auto_refund'),
|
auto_refund=form.cleaned_data.get('auto_refund'),
|
||||||
manual_refund=form.cleaned_data.get('manual_refund'),
|
manual_refund=form.cleaned_data.get('manual_refund'),
|
||||||
refund_as_giftcard=form.cleaned_data.get('refund_as_giftcard'),
|
refund_as_giftcard=form.cleaned_data.get('refund_as_giftcard'),
|
||||||
giftcard_expires=form.cleaned_data.get('gift_card_expires'),
|
giftcard_expires=form.cleaned_data.get('gift_card_expires'),
|
||||||
giftcard_conditions=form.cleaned_data.get('gift_card_conditions'),
|
giftcard_conditions=form.cleaned_data.get('gift_card_conditions'),
|
||||||
keep_fee_fixed=form.cleaned_data.get('keep_fee_fixed'),
|
keep_fee_fixed=form.cleaned_data.get('keep_fee_fixed'),
|
||||||
|
keep_fee_per_ticket=form.cleaned_data.get('keep_fee_per_ticket'),
|
||||||
keep_fee_percentage=form.cleaned_data.get('keep_fee_percentage'),
|
keep_fee_percentage=form.cleaned_data.get('keep_fee_percentage'),
|
||||||
keep_fees=form.cleaned_data.get('keep_fees'),
|
keep_fees=form.cleaned_data.get('keep_fees'),
|
||||||
send=form.cleaned_data.get('send'),
|
send=form.cleaned_data.get('send'),
|
||||||
|
|||||||
@@ -1057,7 +1057,7 @@ class GiftCardDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
r.payment_provider.execute_payment(None, r)
|
r.payment_provider.execute_payment(request, r)
|
||||||
except PaymentException as e:
|
except PaymentException as e:
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
r.state = OrderPayment.PAYMENT_STATE_FAILED
|
r.state = OrderPayment.PAYMENT_STATE_FAILED
|
||||||
@@ -1254,6 +1254,8 @@ class ExportDoView(OrganizerPermissionRequiredMixin, ExportMixin, AsyncAction, V
|
|||||||
user=self.request.user.id,
|
user=self.request.user.id,
|
||||||
fileid=str(cf.id),
|
fileid=str(cf.id),
|
||||||
provider=self.exporter.identifier,
|
provider=self.exporter.identifier,
|
||||||
|
device=None,
|
||||||
|
token=None,
|
||||||
form_data=self.exporter.form.cleaned_data
|
form_data=self.exporter.form.cleaned_data
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from datetime import datetime, timedelta
|
|||||||
from dateutil.rrule import DAILY, MONTHLY, WEEKLY, YEARLY, rrule, rruleset
|
from dateutil.rrule import DAILY, MONTHLY, WEEKLY, YEARLY, rrule, rruleset
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
from django.db import transaction
|
from django.db import connections, transaction
|
||||||
from django.db.models import F, IntegerField, OuterRef, Prefetch, Subquery, Sum
|
from django.db.models import F, IntegerField, OuterRef, Prefetch, Subquery, Sum
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.forms import inlineformset_factory
|
from django.forms import inlineformset_factory
|
||||||
@@ -863,7 +863,13 @@ class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, Crea
|
|||||||
f.subevent = se
|
f.subevent = se
|
||||||
f.save()
|
f.save()
|
||||||
|
|
||||||
|
if connections['default'].features.can_return_rows_from_bulk_insert:
|
||||||
LogEntry.objects.bulk_create(log_entries)
|
LogEntry.objects.bulk_create(log_entries)
|
||||||
|
LogEntry.bulk_postprocess(log_entries)
|
||||||
|
else:
|
||||||
|
for le in log_entries:
|
||||||
|
le.save()
|
||||||
|
LogEntry.bulk_postprocess(log_entries)
|
||||||
|
|
||||||
self.request.event.cache.clear()
|
self.request.event.cache.clear()
|
||||||
messages.success(self.request, pgettext_lazy('subevent', '{} new dates have been created.').format(len(subevents)))
|
messages.success(self.request, pgettext_lazy('subevent', '{} new dates have been created.').format(len(subevents)))
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from django.views import View
|
|||||||
from django.views.generic import FormView, ListView, TemplateView, UpdateView
|
from django.views.generic import FormView, ListView, TemplateView, UpdateView
|
||||||
from django_otp.plugins.otp_static.models import StaticDevice
|
from django_otp.plugins.otp_static.models import StaticDevice
|
||||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from pretix.base.auth import get_auth_backends
|
from pretix.base.auth import get_auth_backends
|
||||||
from pretix.base.forms.auth import ReauthForm
|
from pretix.base.forms.auth import ReauthForm
|
||||||
@@ -576,7 +577,13 @@ class User2FARegenerateEmergencyView(RecentAuthenticationRequiredMixin, Template
|
|||||||
|
|
||||||
|
|
||||||
class UserNotificationsDisableView(TemplateView):
|
class UserNotificationsDisableView(TemplateView):
|
||||||
def get(self, request, *args, **kwargs):
|
template_name = 'pretixcontrol/user/notifications_disable.html'
|
||||||
|
|
||||||
|
@scopes_disabled()
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
user = get_object_or_404(User, notifications_token=kwargs.get('token'), pk=kwargs.get('id'))
|
user = get_object_or_404(User, notifications_token=kwargs.get('token'), pk=kwargs.get('id'))
|
||||||
user.notifications_send = False
|
user.notifications_send = False
|
||||||
user.save()
|
user.save()
|
||||||
|
|||||||
43
src/pretix/helpers/config.py
Normal file
43
src/pretix/helpers/config.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
from configparser import _UNSET
|
||||||
|
|
||||||
|
|
||||||
|
class EnvOrParserConfig:
|
||||||
|
def __init__(self, configparser):
|
||||||
|
self.cp = configparser
|
||||||
|
|
||||||
|
def _envkey(self, section, option):
|
||||||
|
section = re.sub('[^a-zA-Z0-9]', '_', section.upper())
|
||||||
|
option = re.sub('[^a-zA-Z0-9]', '_', option.upper())
|
||||||
|
return f'PRETIX_{section}_{option}'
|
||||||
|
|
||||||
|
def get(self, section, option, *, raw=False, vars=None, fallback=_UNSET):
|
||||||
|
if self._envkey(section, option) in os.environ:
|
||||||
|
return os.environ[self._envkey(section, option)]
|
||||||
|
return self.cp.get(section, option, raw=raw, vars=vars, fallback=fallback)
|
||||||
|
|
||||||
|
def getint(self, section, option, *, raw=False, vars=None, fallback=_UNSET):
|
||||||
|
if self._envkey(section, option) in os.environ:
|
||||||
|
return int(os.environ[self._envkey(section, option)])
|
||||||
|
return self.cp.getint(section, option, raw=raw, vars=vars, fallback=fallback)
|
||||||
|
|
||||||
|
def getfloat(self, section, option, *, raw=False, vars=None, fallback=_UNSET):
|
||||||
|
if self._envkey(section, option) in os.environ:
|
||||||
|
return float(os.environ[self._envkey(section, option)])
|
||||||
|
return self.cp.getfloat(section, option, raw=raw, vars=vars, fallback=fallback)
|
||||||
|
|
||||||
|
def getboolean(self, section, option, *, raw=False, vars=None, fallback=_UNSET):
|
||||||
|
if self._envkey(section, option) in os.environ:
|
||||||
|
return self.cp._convert_to_boolean(os.environ[self._envkey(section, option)])
|
||||||
|
return self.cp.getboolean(section, option, raw=raw, vars=vars, fallback=fallback)
|
||||||
|
|
||||||
|
def has_section(self, section):
|
||||||
|
if any(k.startswith(self._envkey(section, '')) for k in os.environ):
|
||||||
|
return True
|
||||||
|
return self.cp.has_section(section)
|
||||||
|
|
||||||
|
def has_option(self, section, option):
|
||||||
|
if self._envkey(section, option) in os.environ:
|
||||||
|
return True
|
||||||
|
return self.cp.has_option(section, option)
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2020-10-24 20:02+0000\n"
|
"POT-Creation-Date: 2020-11-24 09:10+0000\n"
|
||||||
"PO-Revision-Date: 2020-07-30 19:00+0000\n"
|
"PO-Revision-Date: 2020-07-30 19:00+0000\n"
|
||||||
"Last-Translator: Abdullah <abdullah.gumaijan@gmail.com>\n"
|
"Last-Translator: Abdullah <abdullah.gumaijan@gmail.com>\n"
|
||||||
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2020-10-24 20:02+0000\n"
|
"POT-Creation-Date: 2020-11-24 09:10+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: Automatically generated\n"
|
"Last-Translator: Automatically generated\n"
|
||||||
"Language-Team: none\n"
|
"Language-Team: none\n"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2020-10-24 20:02+0000\n"
|
"POT-Creation-Date: 2020-11-24 09:10+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: Automatically generated\n"
|
"Last-Translator: Automatically generated\n"
|
||||||
"Language-Team: none\n"
|
"Language-Team: none\n"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2020-10-24 20:02+0000\n"
|
"POT-Creation-Date: 2020-11-24 09:10+0000\n"
|
||||||
"PO-Revision-Date: 2020-09-15 02:00+0000\n"
|
"PO-Revision-Date: 2020-09-15 02:00+0000\n"
|
||||||
"Last-Translator: Mie Frydensbjerg <mif@aarhus.dk>\n"
|
"Last-Translator: Mie Frydensbjerg <mif@aarhus.dk>\n"
|
||||||
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2020-10-24 20:02+0000\n"
|
"POT-Creation-Date: 2020-11-24 09:10+0000\n"
|
||||||
"PO-Revision-Date: 2020-08-25 02:00+0000\n"
|
"PO-Revision-Date: 2020-08-25 02:00+0000\n"
|
||||||
"Last-Translator: Dennis Lichtenthäler <lichtenthaeler@rami.io>\n"
|
"Last-Translator: Dennis Lichtenthäler <lichtenthaeler@rami.io>\n"
|
||||||
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ Leaflet
|
|||||||
loszulegen
|
loszulegen
|
||||||
Ltd
|
Ltd
|
||||||
max
|
max
|
||||||
|
Merchandise
|
||||||
Meta
|
Meta
|
||||||
Metadaten
|
Metadaten
|
||||||
Mi
|
Mi
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2020-10-24 20:02+0000\n"
|
"POT-Creation-Date: 2020-11-24 09:10+0000\n"
|
||||||
"PO-Revision-Date: 2020-08-25 02:00+0000\n"
|
"PO-Revision-Date: 2020-08-25 02:00+0000\n"
|
||||||
"Last-Translator: Dennis Lichtenthäler <lichtenthaeler@rami.io>\n"
|
"Last-Translator: Dennis Lichtenthäler <lichtenthaeler@rami.io>\n"
|
||||||
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
|
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ Leaflet
|
|||||||
loszulegen
|
loszulegen
|
||||||
Ltd
|
Ltd
|
||||||
max
|
max
|
||||||
|
Merchandise
|
||||||
Meta
|
Meta
|
||||||
Metadaten
|
Metadaten
|
||||||
Mi
|
Mi
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2020-10-24 20:02+0000\n"
|
"POT-Creation-Date: 2020-11-24 09:10+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2020-10-24 20:02+0000\n"
|
"POT-Creation-Date: 2020-11-24 09:10+0000\n"
|
||||||
"PO-Revision-Date: 2019-10-03 19:00+0000\n"
|
"PO-Revision-Date: 2019-10-03 19:00+0000\n"
|
||||||
"Last-Translator: Chris Spy <chrispiropoulou@hotmail.com>\n"
|
"Last-Translator: Chris Spy <chrispiropoulou@hotmail.com>\n"
|
||||||
"Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2020-10-24 20:02+0000\n"
|
"POT-Creation-Date: 2020-11-24 09:10+0000\n"
|
||||||
"PO-Revision-Date: 2020-04-27 20:00+0000\n"
|
"PO-Revision-Date: 2020-04-27 20:00+0000\n"
|
||||||
"Last-Translator: Gonzalo Gabriel Perez <zalitoar@gmail.com>\n"
|
"Last-Translator: Gonzalo Gabriel Perez <zalitoar@gmail.com>\n"
|
||||||
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
|
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user