forked from CGM_Public/pretix_original
Compare commits
1 Commits
v3.13.1
...
stripe-con
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
951e498089 |
12
Dockerfile
12
Dockerfile
@@ -1,4 +1,4 @@
|
||||
FROM python:3.8
|
||||
FROM python:3.6
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
@@ -30,8 +30,7 @@ RUN apt-get update && \
|
||||
mkdir /data && \
|
||||
useradd -ms /bin/bash -d /pretix -u 15371 pretixuser && \
|
||||
echo 'pretixuser ALL=(ALL) NOPASSWD:SETENV: /usr/bin/supervisord' >> /etc/sudoers && \
|
||||
mkdir /static && \
|
||||
mkdir /etc/supervisord
|
||||
mkdir /static
|
||||
|
||||
ENV LC_ALL=C.UTF-8 \
|
||||
DJANGO_SETTINGS_MODULE=production_settings
|
||||
@@ -48,13 +47,12 @@ RUN pip3 install -U \
|
||||
-r requirements.txt \
|
||||
-r requirements/memcached.txt \
|
||||
-r requirements/mysql.txt \
|
||||
gunicorn django-extensions ipython && \
|
||||
-r requirements/redis.txt \
|
||||
gunicorn && \
|
||||
rm -rf ~/.cache/pip
|
||||
|
||||
COPY deployment/docker/pretix.bash /usr/local/bin/pretix
|
||||
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/supervisord.conf /etc/supervisord.conf
|
||||
COPY deployment/docker/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY deployment/docker/production_settings.py /pretix/src/production_settings.py
|
||||
COPY src /pretix/src
|
||||
|
||||
@@ -5,8 +5,6 @@ export DATA_DIR=/data/
|
||||
export HOME=/pretix
|
||||
export NUM_WORKERS=$((2 * $(nproc --all)))
|
||||
|
||||
AUTOMIGRATE=${AUTOMIGRATE:-yes}
|
||||
|
||||
if [ ! -d /data/logs ]; then
|
||||
mkdir /data/logs;
|
||||
fi
|
||||
@@ -18,16 +16,10 @@ if [ "$1" == "cron" ]; then
|
||||
exec python3 -m pretix runperiodic
|
||||
fi
|
||||
|
||||
if [ "$AUTOMIGRATE" != "skip" ]; then
|
||||
python3 -m pretix migrate --noinput
|
||||
fi
|
||||
python3 -m pretix migrate --noinput
|
||||
|
||||
if [ "$1" == "all" ]; then
|
||||
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
|
||||
exec sudo -E /usr/bin/supervisord -n -c /etc/supervisord.conf
|
||||
fi
|
||||
|
||||
if [ "$1" == "webworker" ]; then
|
||||
@@ -41,12 +33,17 @@ if [ "$1" == "webworker" ]; then
|
||||
fi
|
||||
|
||||
if [ "$1" == "taskworker" ]; then
|
||||
shift
|
||||
exec celery -A pretix.celery_app worker -l info "$@"
|
||||
export C_FORCE_ROOT=True
|
||||
exec celery -A pretix.celery_app worker -l info
|
||||
fi
|
||||
|
||||
if [ "$1" == "shell" ]; then
|
||||
exec python3 -m pretix shell
|
||||
fi
|
||||
|
||||
if [ "$1" == "upgrade" ]; then
|
||||
exec python3 -m pretix updatestyles
|
||||
fi
|
||||
|
||||
exec python3 -m pretix "$@"
|
||||
echo "Specify argument: all|cron|webworker|taskworker|shell|upgrade"
|
||||
exit 1
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
[include]
|
||||
files = /etc/supervisord/*.conf
|
||||
44
deployment/docker/supervisord.conf
Normal file
44
deployment/docker/supervisord.conf
Normal file
@@ -0,0 +1,44 @@
|
||||
[unix_http_server]
|
||||
file=/tmp/supervisor.sock
|
||||
|
||||
[supervisord]
|
||||
logfile=/tmp/supervisord.log
|
||||
logfile_maxbytes=50MB
|
||||
logfile_backups=10
|
||||
loglevel=info
|
||||
pidfile=/tmp/supervisord.pid
|
||||
nodaemon=false
|
||||
minfds=1024
|
||||
minprocs=200
|
||||
|
||||
[rpcinterface:supervisor]
|
||||
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
|
||||
|
||||
[supervisorctl]
|
||||
serverurl=unix:///tmp/supervisor.sock
|
||||
|
||||
[program:pretixweb]
|
||||
command=/usr/local/bin/pretix webworker
|
||||
autostart=true
|
||||
autorestart=true
|
||||
priority=5
|
||||
user=pretixuser
|
||||
environment=HOME=/pretix
|
||||
|
||||
[program:pretixtask]
|
||||
command=/usr/local/bin/pretix taskworker
|
||||
autostart=true
|
||||
autorestart=true
|
||||
priority=5
|
||||
user=pretixuser
|
||||
|
||||
[program:nginx]
|
||||
command=/usr/sbin/nginx
|
||||
autostart=true
|
||||
autorestart=true
|
||||
priority=10
|
||||
stdout_events_enabled=true
|
||||
stderr_events_enabled=true
|
||||
|
||||
[include]
|
||||
files = /etc/supervisord-*.conf
|
||||
@@ -1,2 +0,0 @@
|
||||
[include]
|
||||
files = /etc/supervisord/base.conf /etc/supervisord/nginx.conf /etc/supervisord/pretixweb.conf
|
||||
@@ -1,18 +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
|
||||
@@ -1,7 +0,0 @@
|
||||
[program:nginx]
|
||||
command=/usr/sbin/nginx
|
||||
autostart=true
|
||||
autorestart=true
|
||||
priority=10
|
||||
stdout_events_enabled=true
|
||||
stderr_events_enabled=true
|
||||
@@ -1,6 +0,0 @@
|
||||
[program:pretixtask]
|
||||
command=/usr/local/bin/pretix taskworker
|
||||
autostart=true
|
||||
autorestart=true
|
||||
priority=5
|
||||
user=pretixuser
|
||||
@@ -1,7 +0,0 @@
|
||||
[program:pretixweb]
|
||||
command=/usr/local/bin/pretix webworker
|
||||
autostart=true
|
||||
autorestart=true
|
||||
priority=5
|
||||
user=pretixuser
|
||||
environment=HOME=/pretix
|
||||
@@ -6099,6 +6099,3 @@ img.screenshot, a.screenshot img {
|
||||
.versionchanged p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.rst-content td > .line-block {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
@@ -23,14 +23,6 @@ The config file may contain the following sections (all settings are optional an
|
||||
default values). We suggest that you start from the examples given in one of the
|
||||
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
|
||||
---------------
|
||||
|
||||
@@ -107,7 +99,7 @@ Example::
|
||||
Log violations of the Content Security Policy (CSP). Defaults to ``on``.
|
||||
|
||||
``loglevel``
|
||||
Set console and file log level (``DEBUG``, ``INFO``, ``WARNING``, ``ERROR`` or ``CRITICAL``). Defaults to ``INFO``.
|
||||
Set console and file loglevel (``DEBUG``, ``INFO``, ``WARNING``, ``ERROR`` or ``CRITICAL``). Defaults to ``INFO``.
|
||||
|
||||
Locale settings
|
||||
---------------
|
||||
|
||||
@@ -284,24 +284,6 @@ Then, go to that directory and build the image::
|
||||
You can now use that image ``mypretix`` instead of ``pretix/standalone`` in your service file (see above). Be sure
|
||||
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/
|
||||
.. _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/
|
||||
|
||||
@@ -49,15 +49,11 @@ information on your device as well as your API token:
|
||||
"device_id": 5,
|
||||
"unique_serial": "HHZ9LW9JWP390VFZ",
|
||||
"api_token": "1kcsh572fonm3hawalrncam4l1gktr2rzx25a22l8g9hx108o9oi0rztpcvwnfnd",
|
||||
"name": "Bar",
|
||||
"gate": {
|
||||
"id": 3,
|
||||
"name": "South entrance"
|
||||
}
|
||||
"name": "Bar"
|
||||
}
|
||||
|
||||
Please make sure that you store this ``api_token`` value. We also recommend storing your device ID, your assigned
|
||||
``unique_serial``, and the ``organizer`` you have access to, but that's up to you. ``gate`` might be ``null``.
|
||||
``unique_serial``, and the ``organizer`` you have access to, but that's up to you.
|
||||
|
||||
In case of an error, the response will look like this:
|
||||
|
||||
@@ -102,8 +98,6 @@ following endpoint:
|
||||
"software_version": "4.1.0"
|
||||
}
|
||||
|
||||
You will receive a response equivalent to the response of your initialization request.
|
||||
|
||||
Creating a new API key
|
||||
----------------------
|
||||
|
||||
@@ -132,65 +126,12 @@ invalidate your API key. There is no way to reverse this operation.
|
||||
|
||||
This can also be done by the user through the web interface.
|
||||
|
||||
Permissions & security profiles
|
||||
-------------------------------
|
||||
Permissions
|
||||
-----------
|
||||
|
||||
Device authentication is currently hardcoded to grant the following permissions:
|
||||
|
||||
* View event meta data and products etc.
|
||||
* View orders
|
||||
* Change orders
|
||||
* Manage gift cards
|
||||
* View and change orders
|
||||
|
||||
Devices cannot change events or products and cannot access vouchers.
|
||||
|
||||
Additionally, when creating a device through the user interface or API, a user can specify a "security profile" for
|
||||
the device. These include an allow list of specific API calls that may be made by the device. pretix ships with security
|
||||
policies for official pretix apps like pretixSCAN and pretixPOS.
|
||||
|
||||
Removing a device
|
||||
-----------------
|
||||
|
||||
If you want implement a way to to deprovision a device in your software, you can call the ``revoke`` endpoint to
|
||||
invalidate your API key. There is no way to reverse this operation.
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/device/revoke HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Authorization: Device 1kcsh572fonm3hawalrncam4l1gktr2rzx25a22l8g9hx108o9oi0rztpcvwnfnd
|
||||
|
||||
This can also be done by the user through the web interface.
|
||||
|
||||
Event selection
|
||||
---------------
|
||||
|
||||
In most cases, your application should allow the user to select the event and check-in list they work with manually
|
||||
from a list. However, in some cases it is required to automatically configure the device for the correct event, for
|
||||
example in a kiosk-like situation where nobody is operating the device. In this case, the app can query the server
|
||||
for a suggestion which event should be used. You can also submit the configuration that is currently in use via
|
||||
query parameters:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/device/eventselection?current_event=democon¤t_subevent=42¤t_checkinlist=542 HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Authorization: Device 1kcsh572fonm3hawalrncam4l1gktr2rzx25a22l8g9hx108o9oi0rztpcvwnfnd
|
||||
|
||||
You can get three response codes:
|
||||
|
||||
* ``304`` The server things you already selected a good event
|
||||
* ``404`` The server has not found a suggestion for you
|
||||
* ``200`` The server suggests a new event (body see below)
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"event": "democon",
|
||||
"subevent": 23,
|
||||
"checkinlist": 5
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ On a failed registration, a query string like ``?error=access_denied`` will be a
|
||||
|
||||
.. note:: By default, the user is asked to give permission on every call to this URL. If you **only** request the
|
||||
``profile`` scope, i.e. no access to organizer data, you can pass the ``approval_prompt=auto`` parameter
|
||||
to skip user interaction on subsequent calls.
|
||||
to skip user interaction on subsequen calls.
|
||||
|
||||
Getting an access token
|
||||
-----------------------
|
||||
|
||||
@@ -33,7 +33,6 @@ auto_checkin_sales_channels list of strings All items on th
|
||||
allow_multiple_entries boolean If ``true``, subsequent scans of a ticket on this list should not show a warning but instead be stored as an additional check-in.
|
||||
allow_entry_after_exit boolean If ``true``, subsequent scans of a ticket on this list are valid if the last scan of the ticket was an exit scan.
|
||||
rules object Custom check-in logic. The contents of this field are currently not considered a stable API and modifications through the API are highly discouraged.
|
||||
exit_all_at datetime Automatically check out (i.e. perform an exit scan) at this point in time. After this happened, this property will automatically be set exactly one day into the future. Note that this field is considered "internal configuration" and if you pull the list with ``If-Modified-Since``, the daily change in this field will not trigger a response.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.10
|
||||
@@ -61,10 +60,6 @@ exit_all_at datetime Automatically c
|
||||
|
||||
The ``subevent_match`` and ``exclude`` query parameters have been added.
|
||||
|
||||
.. versionchanged:: 3.12
|
||||
|
||||
The ``exit_all_at`` attribute has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -108,7 +103,6 @@ Endpoints
|
||||
"subevent": null,
|
||||
"allow_multiple_entries": false,
|
||||
"allow_entry_after_exit": true,
|
||||
"exit_all_at": null,
|
||||
"rules": {},
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
@@ -158,7 +152,6 @@ Endpoints
|
||||
"subevent": null,
|
||||
"allow_multiple_entries": false,
|
||||
"allow_entry_after_exit": true,
|
||||
"exit_all_at": null,
|
||||
"rules": {},
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
@@ -195,7 +188,6 @@ Endpoints
|
||||
{
|
||||
"checkin_count": 17,
|
||||
"position_count": 42,
|
||||
"inside_count": 12,
|
||||
"event": {
|
||||
"name": "Demo Conference"
|
||||
},
|
||||
|
||||
@@ -216,7 +216,7 @@ Device endpoints
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param device_id: The ``device_id`` field of the device to modify
|
||||
:param device_id: The ``device_id`` field of the deviec to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The device could not be modified due to invalid submitted data
|
||||
:statuscode 401: Authentication failure
|
||||
|
||||
@@ -44,9 +44,6 @@ seat_category_mapping object An object mappi
|
||||
(strings) to items in the event (integers or ``null``).
|
||||
timezone string Event timezone name
|
||||
item_meta_properties object Item-specific meta data parameters and default values.
|
||||
valid_keys object Cryptographic keys for non-default signature schemes.
|
||||
For performance reason, value is omitted in lists and
|
||||
only contained in detail views. Value can be cached.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
@@ -87,10 +84,6 @@ valid_keys object Cryptographic k
|
||||
|
||||
The attribute ``item_meta_properties`` has been added.
|
||||
|
||||
.. versionchanged:: 3.12
|
||||
|
||||
The attribute ``valid_keys`` has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -151,7 +144,7 @@ Endpoints
|
||||
"pretix.plugins.stripe"
|
||||
"pretix.plugins.paypal"
|
||||
"pretix.plugins.ticketoutputpdf"
|
||||
],
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -223,12 +216,7 @@ Endpoints
|
||||
"pretix.plugins.stripe"
|
||||
"pretix.plugins.paypal"
|
||||
"pretix.plugins.ticketoutputpdf"
|
||||
],
|
||||
"valid_keys": {
|
||||
"pretix_sig1": [
|
||||
"LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUNvd0JRWURLMlZ3QXlFQTdBRDcvdkZBMzNFc1k0ejJQSHI3aVpQc1o4bjVkaDBhalA4Z3l6Tm1tSXM9Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo="
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
@@ -484,7 +472,7 @@ Endpoints
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
|
||||
|
||||
|
||||
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/
|
||||
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/items/(id)/
|
||||
|
||||
Delete an event. Note that events with orders cannot be deleted to ensure data integrity.
|
||||
|
||||
|
||||
@@ -1,215 +0,0 @@
|
||||
.. spelling:: checkin
|
||||
|
||||
Data exporters
|
||||
==============
|
||||
|
||||
pretix and it's plugins include a number of data exporters that allow you to bulk download various data from pretix in
|
||||
different formats. This page shows you how to use these exporters through the API.
|
||||
|
||||
.. versionchanged:: 3.13
|
||||
|
||||
This feature has been added to the API.
|
||||
|
||||
.. warning::
|
||||
|
||||
While we consider the methods listed on this page to be a stable API, the availability and specific input field
|
||||
requirements of individual exporters is **not considered a stable API**. Specific exporters and their input parameters
|
||||
may change at any time without warning.
|
||||
|
||||
Listing available exporters
|
||||
---------------------------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/exporters/
|
||||
|
||||
Returns a list of all exporters available for a given event. You will receive a list of export methods as well as their
|
||||
supported input fields. Note that the exact type and validation requirements of the input fields are not given in the
|
||||
response, and you might need to look into the pretix web interface to figure out the exact input required.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/exporters/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"identifier": "orderlist",
|
||||
"verbose_name": "Order data",
|
||||
"input_parameters": [
|
||||
{
|
||||
"name": "_format",
|
||||
"required": true,
|
||||
"choices": [
|
||||
"xlsx",
|
||||
"orders:default",
|
||||
"orders:excel",
|
||||
"orders:semicolon",
|
||||
"positions:default",
|
||||
"positions:excel",
|
||||
"positions:semicolon",
|
||||
"fees:default",
|
||||
"fees:excel",
|
||||
"fees:semicolon"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "paid_only",
|
||||
"required": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/exporters/
|
||||
|
||||
Returns a list of all cross-event exporters available for a given organizer. You will receive a list of export methods as well as their
|
||||
supported input fields. Note that the exact type and validation requirements of the input fields are not given in the
|
||||
response, and you might need to look into the pretix web interface to figure out the exact input required.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/exporters/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"identifier": "orderlist",
|
||||
"verbose_name": "Order data",
|
||||
"input_parameters": [
|
||||
{
|
||||
"name": "events",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "_format",
|
||||
"required": true,
|
||||
"choices": [
|
||||
"xlsx",
|
||||
"orders:default",
|
||||
"orders:excel",
|
||||
"orders:semicolon",
|
||||
"positions:default",
|
||||
"positions:excel",
|
||||
"positions:semicolon",
|
||||
"fees:default",
|
||||
"fees:excel",
|
||||
"fees:semicolon"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "paid_only",
|
||||
"required": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
|
||||
Running an export
|
||||
-----------------
|
||||
|
||||
Since exports often include large data sets, they might take longer than the duration of an HTTP request. Therefore,
|
||||
creating an export is a two-step process. First you need to start an export task with one of the following to API
|
||||
endpoints:
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/exporters/(identifier)/run/
|
||||
|
||||
Starts an export task. If your input parameters validate correctly, a ``202 Accepted`` status code is returned.
|
||||
The body points you to the download URL of the result.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/exporters/orderlist/run/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"_format": "xlsx"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"download": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/orderlist/download/29891ede-196f-4942-9e26-d055a36e98b8/3f279f13-c198-4137-b49b-9b360ce9fcce/"
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:param identifier: The ``identifier`` field of the exporter to run
|
||||
:statuscode 202: no error
|
||||
:statuscode 400: Invalid input options
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/exporters/(identifier)/run/
|
||||
|
||||
The endpoint for organizer-level exports works just like event-level exports (see above).
|
||||
|
||||
|
||||
Downloading the result
|
||||
----------------------
|
||||
|
||||
When starting an export, you receive a ``url`` for downloading the result. Running a ``GET`` request on that result will
|
||||
yield one of the following status codes:
|
||||
|
||||
* ``200 OK`` – The export succeeded. The body will be your resulting file. Might be large!
|
||||
* ``409 Conflict`` – Your export is still running. The body will be JSON with the structure ``{"status": "running", "percentage": 40}``. ``percentage`` can be ``null`` if it is not known and ``status`` can be ``waiting`` before the task is actually being processed. Please retry, but wait at least one second before you do.
|
||||
* ``410 Gone`` – Running the export has failed permanently. The body will be JSON with the structure ``{"status": "failed", "message": "Error message"}``
|
||||
* ``404 Not Found`` – The export does not exist / is expired.
|
||||
|
||||
.. warning::
|
||||
|
||||
Running exports puts a lot of stress on the system, we kindly ask you not to run more than two exports at the same time.
|
||||
|
||||
@@ -27,6 +27,5 @@ Resources and endpoints
|
||||
devices
|
||||
webhooks
|
||||
seatingplans
|
||||
exporters
|
||||
billing_invoices
|
||||
billing_var
|
||||
|
||||
@@ -163,10 +163,6 @@ last_modified datetime Last modificati
|
||||
|
||||
The ``exclude`` and ``subevent_after`` query parameter has been added.
|
||||
|
||||
.. versionchanged:: 3.13
|
||||
|
||||
The ``subevent_before`` query parameter has been added.
|
||||
|
||||
|
||||
.. _order-position-resource:
|
||||
|
||||
@@ -494,8 +490,7 @@ List of all orders
|
||||
recommend using this in combination with ``testmode=false``, since test mode orders can vanish at any time and
|
||||
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 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 datetime subevent_after: Only return orders that contain a ticket for a subevent taking place after the given date.
|
||||
: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 event: The ``slug`` field of the event to fetch
|
||||
@@ -2268,57 +2263,3 @@ Order refund endpoints
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
:statuscode 404: The requested order or refund does not exist.
|
||||
|
||||
Revoked ticket secrets
|
||||
----------------------
|
||||
|
||||
With some non-default ticket secret generation methods, a list of revoked ticket secrets is required for proper validation.
|
||||
|
||||
.. versionchanged:: 3.12
|
||||
|
||||
Added revocation lists.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/revokedsecrets/
|
||||
|
||||
Returns a list of all revoked secrets within a given event.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/revokedsecrets/ 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
|
||||
X-Page-Generated: 2017-12-01T10:00:00Z
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 1234,
|
||||
"secret": "k24fiuwvu8kxz3y1",
|
||||
"created": "2017-12-01T10:00:00Z",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``secret`` and ``created``. Default: ``-created``
|
||||
:query datetime created_since: Only return revocations that have been created since the given date.
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:resheader X-Page-Generated: The server time at the beginning of the operation. If you're using this API to fetch
|
||||
differences, this is the value you want to use as ``created_since`` in your next call.
|
||||
: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.
|
||||
|
||||
@@ -51,7 +51,6 @@ seating_plan integer If reserved sea
|
||||
plan. Otherwise ``null``.
|
||||
seat_category_mapping object An object mapping categories of the seating plan
|
||||
(strings) to items in the event (integers or ``null``).
|
||||
last_modified datetime Last modification of this object
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
@@ -81,10 +80,6 @@ last_modified datetime Last modificati
|
||||
|
||||
The ``disabled`` attribute has been added to ``item_price_overrides`` and ``variation_price_overrides``.
|
||||
|
||||
.. versionchanged:: 3.12
|
||||
|
||||
The ``last_modified`` attribute has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -153,8 +148,6 @@ Endpoints
|
||||
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned.
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:param event: The ``slug`` field of the main event
|
||||
:query datetime modified_since: Only return objects that have changed since the given date. Be careful: This does not
|
||||
allow you to know if a subevent was deleted.
|
||||
:query array attr[meta_data_key]: By providing the key and value of a meta data attribute, the list of sub-events
|
||||
will only contain the sub-events matching the set criteria. Providing ``?attr[Format]=Seminar`` would return
|
||||
only those sub-events having set their ``Format`` meta data to ``Seminar``, ``?attr[Format]=`` only those, that
|
||||
|
||||
@@ -31,10 +31,8 @@ action_types list of strings A list of actio
|
||||
The following values for ``action_types`` are valid with pretix core:
|
||||
|
||||
* ``pretix.event.order.placed``
|
||||
* ``pretix.event.order.placed.require_approval``
|
||||
* ``pretix.event.order.paid``
|
||||
* ``pretix.event.order.canceled``
|
||||
* ``pretix.event.order.reactivated``
|
||||
* ``pretix.event.order.expired``
|
||||
* ``pretix.event.order.modified``
|
||||
* ``pretix.event.order.contact.changed``
|
||||
@@ -44,12 +42,6 @@ The following values for ``action_types`` are valid with pretix core:
|
||||
* ``pretix.event.order.denied``
|
||||
* ``pretix.event.checkin``
|
||||
* ``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.
|
||||
|
||||
|
||||
@@ -52,7 +52,6 @@ extensions = [
|
||||
'sphinx.ext.coverage',
|
||||
'sphinxcontrib.httpdomain',
|
||||
'sphinxcontrib.images',
|
||||
'sphinxemoji.sphinxemoji',
|
||||
]
|
||||
if HAS_PYENCHANT:
|
||||
extensions.append('sphinxcontrib.spelling')
|
||||
|
||||
@@ -12,8 +12,7 @@ Core
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types,
|
||||
item_copy_data, register_sales_channels, register_global_settings, quota_availability, global_email_filter,
|
||||
register_ticket_secret_generators
|
||||
item_copy_data, register_sales_channels, register_global_settings, quota_availability, global_email_filter
|
||||
|
||||
Order events
|
||||
""""""""""""
|
||||
@@ -34,7 +33,7 @@ Frontend
|
||||
--------
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, contact_form_fields_overrides, question_form_fields_overrides, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header
|
||||
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header
|
||||
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
@@ -58,7 +57,7 @@ Backend
|
||||
.. automodule:: pretix.control.signals
|
||||
: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,
|
||||
item_formsets, order_search_filter_q, order_search_forms
|
||||
item_formsets, order_search_filter_q
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:members: logentry_display, logentry_object_link, requiredaction_display, timeline_events
|
||||
|
||||
@@ -117,7 +117,7 @@ for example, to check for any errors in any staged files when committing::
|
||||
export GIT_WORK_TREE=../
|
||||
export GIT_DIR=../.git
|
||||
source ../env/bin/activate # Adjust to however you activate your virtual environment
|
||||
for file in $(git diff --cached --name-only | grep -E '\.py$' | grep -Ev "migrations|mt940\.py|pretix/settings\.py|make_testdata\.py|testutils/settings\.py|tests/settings\.py|pretix/base/models/__init__\.py|.*_pb2\.py")
|
||||
for file in $(git diff --cached --name-only | grep -E '\.py$' | grep -Ev "migrations|mt940\.py|pretix/settings\.py|make_testdata\.py|testutils/settings\.py|tests/settings\.py|pretix/base/models/__init__\.py")
|
||||
do
|
||||
echo $file
|
||||
git show ":$file" | flake8 - --stdin-display-name="$file" || exit 1 # we only want to lint the staged changes, not any un-staged changes
|
||||
|
||||
@@ -34,8 +34,6 @@ transactions list of objects Transactions in
|
||||
├ payer string Payment source
|
||||
├ reference string Payment reference
|
||||
├ amount string Payment amount
|
||||
├ iban string Payment IBAN
|
||||
├ bic string Payment BIC
|
||||
├ date string Payment date (in **user-inputted** format)
|
||||
├ order string Associated order code (or ``null``)
|
||||
└ comment string Internal comment
|
||||
@@ -85,8 +83,6 @@ Endpoints
|
||||
"date": "26.06.2017",
|
||||
"payer": "John Doe",
|
||||
"order": null,
|
||||
"iban": "",
|
||||
"bic": "",
|
||||
"checksum": "5de03a601644dfa63420dacfd285565f8375a8f2",
|
||||
"reference": "GUTSCHRIFT\r\nSAMPLECONF-NAB12 EREF: SAMPLECONF-NAB12\r\nIBAN: DE1234556…",
|
||||
"state": "nomatch",
|
||||
@@ -136,8 +132,6 @@ Endpoints
|
||||
"comment": "",
|
||||
"date": "26.06.2017",
|
||||
"payer": "John Doe",
|
||||
"iban": "",
|
||||
"bic": "",
|
||||
"order": null,
|
||||
"checksum": "5de03a601644dfa63420dacfd285565f8375a8f2",
|
||||
"reference": "GUTSCHRIFT\r\nSAMPLECONF-NAB12 EREF: SAMPLECONF-NAB12\r\nIBAN: DE1234556…",
|
||||
|
||||
@@ -3,8 +3,7 @@ sphinx==2.3.*
|
||||
sphinx-rtd-theme
|
||||
sphinxcontrib-httpdomain
|
||||
sphinxcontrib-images
|
||||
sphinxcontrib-spelling==4.*
|
||||
sphinxemoji
|
||||
sphinxcontrib-spelling
|
||||
pygments-markdown-lexer
|
||||
# See https://github.com/rfk/pyenchant/pull/130
|
||||
git+https://github.com/raphaelm/pyenchant.git@patch-1#egg=pyenchant
|
||||
|
||||
@@ -10,11 +10,7 @@ availabilities
|
||||
backend
|
||||
backends
|
||||
banktransfer
|
||||
barcode
|
||||
barcodes
|
||||
Bcc
|
||||
bic
|
||||
BIC
|
||||
boolean
|
||||
booleans
|
||||
cancelled
|
||||
@@ -51,15 +47,12 @@ gunicorn
|
||||
guid
|
||||
hardcoded
|
||||
hostname
|
||||
iban
|
||||
IBAN
|
||||
ics
|
||||
idempotency
|
||||
iframe
|
||||
incrementing
|
||||
inofficial
|
||||
invalidations
|
||||
iOS
|
||||
iterable
|
||||
Jimdo
|
||||
jwt
|
||||
@@ -98,9 +91,7 @@ prepending
|
||||
preprocessor
|
||||
presale
|
||||
pretix
|
||||
pretixSCAN
|
||||
pretixdroid
|
||||
pretixPOS
|
||||
pretixpresale
|
||||
prometheus
|
||||
proxied
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
Warengutschein
|
||||
Wertgutschein
|
||||
|
||||
.. _giftcards:
|
||||
|
||||
Gift cards
|
||||
==========
|
||||
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
Ticket secret generators
|
||||
========================
|
||||
|
||||
pretix allows you to change the way in which ticket secrets (also known as "ticket codes", "barcodes", …)
|
||||
are generated. This affects the value of the QR code in any tickets issued by pretix, regardless of ticket
|
||||
format.
|
||||
|
||||
.. note:: This is intended for highly advanced use cases, usually when huge numbers of tickets (> 25k per event)
|
||||
are involved. **If you don't know whether you need this, you probably don't.**
|
||||
|
||||
Default: Random secrets
|
||||
-----------------------
|
||||
|
||||
By default, pretix generates a random code for every ticket, consisting of 32 lower case characters and
|
||||
numbers. The characters ``oO1il`` are avoided to reduce confusion when ticket codes are printed and need to
|
||||
be typed in manually.
|
||||
|
||||
Choosing random codes has a number of advantages:
|
||||
|
||||
* Ticket codes are short, which makes QR codes easier to scan. At the same time, it is absolutely impossible to
|
||||
guess or forge a valid ticket code.
|
||||
|
||||
* The code does not need to change if the ticket changes. For example, if an attendee is re-booked to a
|
||||
different product or date, they can keep their ticket and it is just mapped to the new product in the
|
||||
database.
|
||||
|
||||
This approach works really well for 99 % or events running with pretix.
|
||||
The big caveat is that the scanner needs to access a database of all ticket codes in order to know whether a ticket
|
||||
code is valid and what kind of ticket it represents.
|
||||
|
||||
When scanning online this is no problem at all, since the pretix server always has such a database. In case your local
|
||||
internet connection is interrupted or the pretix server goes down, though, there needs to be a database locally on the
|
||||
scanner.
|
||||
|
||||
Therefore, our pretixSCAN apps by default download the database of all valid tickets onto the device itself. This makes
|
||||
it possible to seamlessly switch into offline mode when the connection is lost and continue scanning with the maximum
|
||||
possible feature set.
|
||||
|
||||
There are a few situations in which this approach is not ideal:
|
||||
|
||||
* When running a single event with 25k or more valid tickets, downloading all ticket data onto the scanner may just
|
||||
take too much time and resources.
|
||||
|
||||
* When the risk of losing sensible data by losing one of the scanner devices is not acceptable.
|
||||
|
||||
* When offline mode needs to be used regularly and newly-purchased tickets need to be valid immediately after purchase,
|
||||
without being able to tolerate a few minutes of delay.
|
||||
|
||||
Signature schemes
|
||||
-----------------
|
||||
|
||||
The alternative approach that is included with pretix is to choose a signature-based ticket code generation scheme.
|
||||
These secrets include the most important information that is required for verifying their validity and use modern
|
||||
cryptography to make sure they cannot be forged.
|
||||
|
||||
Currently, pretix ships with one such scheme ("pretix signature scheme 1") which encodes the product, the product
|
||||
variation, and the date (if inside an event series) into the ticket code and signs the code with a `EdDSA`_ signature.
|
||||
This allows to verify whether a ticket is allowed to enter without any database or connection to the server, but has
|
||||
a few important drawbacks:
|
||||
|
||||
* Whenever the product, variation or date of a ticket changes or the ticket is canceled, the ticket code needs to be
|
||||
changed and the old code needs to be put on a revocation list. This revocation list again needs to be downloaded by
|
||||
all scanning devices (but is usually much smaller than the ticket database). The main downside is that the attendee
|
||||
needs to download their new ticket and can no longer use the old one.
|
||||
|
||||
* Scanning in offline mode is much more limited, since the scanner has no information about previous usages of the
|
||||
ticket, attendee names, seating information, etc.
|
||||
|
||||
Comparison of scanning behavior
|
||||
-------------------------------
|
||||
|
||||
=============================================== =================================== =================================== =================================== ================================= =====================================
|
||||
Scan mode Online Offline
|
||||
----------------------------------------------- ----------------------------------- -----------------------------------------------------------------------------------------------------------------------------------------------
|
||||
Synchronization setting any Synchronize orders Don't synchronize orders
|
||||
----------------------------------------------- ----------------------------------- ----------------------------------------------------------------------- -----------------------------------------------------------------------
|
||||
Ticket secrets any Random Signed Random Signed
|
||||
=============================================== =================================== =================================== =================================== ================================= =====================================
|
||||
Scenario supported on platforms Android, Desktop, iOS Android, Desktop, iOS Android, Desktop Android, Desktop Android, Desktop
|
||||
Synchronization speed for large data sets slow slow fast fast
|
||||
Tickets can be scanned yes yes yes no yes
|
||||
Ticket is valid after sale immediately next sync (~5 minutes) immediately never immediately
|
||||
Same ticket can be scanned multiple times no yes, before data is synced yes, before data is synced n/a yes, always
|
||||
Custom check-in rules yes yes yes (limited directly after sale) n/a yes, but only based on product,
|
||||
variation and date, not on previous
|
||||
scans
|
||||
Name and seat visible on scanner yes yes yes (except directly after sale) n/a no
|
||||
Order-specific check-in attention flag yes yes yes (except directly after sale) n/a no
|
||||
Ticket search by order code or name yes yes yes (except directly after sale) no no
|
||||
Check-in statistics on scanner yes yes mostly accurate no no
|
||||
=============================================== =================================== =================================== =================================== ================================= =====================================
|
||||
|
||||
.. _EdDSA: https://en.wikipedia.org/wiki/EdDSA#Ed25519
|
||||
@@ -9,33 +9,26 @@ At "Settings" → "Tickets", you can configure the ticket download options that
|
||||
|
||||
The top of this page shows a short list of options relevant for all download formats:
|
||||
|
||||
Allow users to download tickets
|
||||
Use feature
|
||||
This can be used to completely enable or disable ticket downloads all over your ticket shop.
|
||||
|
||||
Generate tickets for add-on products
|
||||
By default, tickets can not be downloaded for order positions which are only an add-on to other order positions. If
|
||||
you enable this, this behavior will be changed and add-on products will get their own tickets as well. If disabled,
|
||||
you can still print a list of chosen add-ons e.g. on the PDF tickets.
|
||||
|
||||
Generate tickets for all products
|
||||
By default, tickets will only be generated for products that are marked as admission products. Enable this option to
|
||||
generate tickets for all products instead.
|
||||
|
||||
Generate tickets for pending orders
|
||||
By default, ticket download is only possible for paid orders. If you run an event where people usually pay only after
|
||||
the event, you can check this box to enable ticket download even before.
|
||||
|
||||
Download date
|
||||
If you set a date here, no ticket download will be offered before this date. If no date is set, tickets can be
|
||||
downloaded immediately after the payment for an order has been received.
|
||||
|
||||
Offer to download tickets separately for add-on products
|
||||
By default, tickets can not be downloaded for order positions which are only an add-on to other order positions. If
|
||||
you enable this, this behavior will be changed and add-on products will get their own tickets as well. If disabled,
|
||||
you can still print a list of chosen add-ons e.g. on the PDF tickets.
|
||||
|
||||
Generate tickets for non-admission products
|
||||
By default, tickets will only be generated for products that are marked as admission products. Enable this option to
|
||||
generate tickets for all products instead.
|
||||
|
||||
Offer to download tickets even before an order is paid
|
||||
By default, ticket download is only possible for paid orders. If you run an event where people usually pay only after
|
||||
the event, you can check this box to enable ticket download even before.
|
||||
|
||||
Below these settings, the detail settings for the various ticket file formats are offered. They differ from format to
|
||||
format and only share the common "Enable" setting that can be used to turn them on. By default, pretix ships with
|
||||
a PDF output plugin that you can configure through a visual design editor.
|
||||
|
||||
**Advanced topics:**
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
ticket_secrets
|
||||
a PDF output plugin that you can configure through a visual design editor.
|
||||
@@ -1,5 +1,3 @@
|
||||
.. _widget:
|
||||
|
||||
Embeddable Widget
|
||||
=================
|
||||
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
Glossary
|
||||
========
|
||||
|
||||
This page gives definitions of domain-specific terms that we use a lot inside pretix and that might be used slightly
|
||||
differently elsewhere, as well as their official translations to other languages. In some cases, things have a different
|
||||
name internally, which is noted with a |:wrench:| symbol. If you only use pretix, you'll never see these, but if you're
|
||||
going to develop around pretix, for example connect to pretix through our API, you need to know these as well.
|
||||
|
||||
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
.. list-table:: Glossary
|
||||
:widths: 15 30
|
||||
:header-rows: 1
|
||||
|
||||
* - Term
|
||||
- Definition
|
||||
* - | |:gb:| **Organizer**
|
||||
| |:de:| Veranstalter
|
||||
- An organizer represents the entity using pretix, usually the company or institution running one or multiple events.
|
||||
In terms of navigation in the system, organizers are the "middle layer" between the system itself and the specific
|
||||
events.
|
||||
Multiple organizers on the same pretix system are fully separated from each other with very few exceptions.
|
||||
* - | |:gb:| **Event**
|
||||
| |:de:| Veranstaltung
|
||||
- An event is the central entity in pretix that you and your customers interact with all the time. An event
|
||||
represents one **shop** in which things like tickets can be bought. Since the introduction of event series (see
|
||||
below), this might include multiple events in the real world.
|
||||
|
||||
Every purchase needs to be connected to an event, and most things are completely separate between different
|
||||
events, i.e. most actions and configurations in pretix are done per-event.
|
||||
* - | |:gb:| **Event series**
|
||||
| |:de:| Veranstaltungsreihe
|
||||
- An event series is one of two types of events. Unlike a non-series event, an event series groups together
|
||||
multiple real-world events into one pretix shop. Examples are time-slot-based booking for a museum, a band on
|
||||
tour, a theater group playing the same play multiple times, etc.
|
||||
* - | |:gb:| **Date**
|
||||
| |:de:| Termin
|
||||
| |:wrench:| Subevent
|
||||
- A date represents a single real-world event inside an event series. Dates can differ from each other in name,
|
||||
date, time, location, pricing, capacity, and seating plans, but otherwise share the same configuration.
|
||||
* - | |:gb:| **Product**
|
||||
| |:de:| Produkt
|
||||
| |:wrench:| Item
|
||||
- A product is anything that can be sold, such as a specific type of ticket or merchandise.
|
||||
* - | |:gb:| **Admission product**
|
||||
| |:de:| Zutrittsprodukt
|
||||
- A product is considered an **admission product** if its purchase represents a person being granted access to your
|
||||
event. This applies to most ticketing products, but not e.g. to merchandise.
|
||||
* - | |:gb:| **Variation**
|
||||
| |:de:| Variante
|
||||
| |:wrench:| Item variation
|
||||
- Some products come in multiple variations that can differ in description, price and capacity. Examples would
|
||||
include "Adult" and "Child" in case of a concert ticket, or "S", "M", "L", … in case of a t-shirt product.
|
||||
* - | |:gb:| **Category**
|
||||
| |:de:| Kategorie
|
||||
- Products can be grouped together in categories. This is mostly to organize them cleanly in the frontend if you
|
||||
have lots of them.
|
||||
* - | |:gb:| **Quota**
|
||||
| |:de:| Kontingent
|
||||
- A quota is a capacity pool that defines how many times a product can be sold. A quota can be connected to multiple
|
||||
products, in which case all of them are counted together. This is useful e.g. if you have full-price and reduced
|
||||
tickets and only want to sell a certain number of tickets in total. The same way, multiple quotas can be connected
|
||||
to the same product, in which case the ticket will be available as long as all of them have capacity left.
|
||||
* - | |:gb:| **Add-on product**
|
||||
| |:de:| Zusatzprodukt
|
||||
- An add-on product is a product that is purchased as an upgrade or optional addition to a different product.
|
||||
Examples would be include a conference ticket that optionally allows to buy a public transport ticket for the
|
||||
same day, or a family ticket for 4 persons that allows you to add additional persons at a small cost, or a
|
||||
"two workshops" package that allows you to select two of a larger number of workshops at a discounted price.
|
||||
In all cases, there is a "main product" (the conference ticket, the family ticket) and a number of "add-on products"
|
||||
that can be chosen from.
|
||||
* - | |:gb:| **Bundled product**
|
||||
| |:de:| Enthaltenes Produkt
|
||||
- A bundled product is a product that is automatically put into the cart when another product is purchased. It's
|
||||
similar to an add-on product, except that the customer has no choice between whether it is added or which of a
|
||||
set of product is added.
|
||||
* - | |:gb:| **Question**
|
||||
| |:de:| Frage
|
||||
- A question is a custom field that customers need to fill in when purchasing a specific product.
|
||||
* - | |:gb:| **Voucher**
|
||||
| |:de:| Gutschein
|
||||
- A voucher is a code that can be used for multiple purposes: To grant a discount to specific customers, to only
|
||||
show certain products to certain customers, or to keep a seat open for someone specific even though you are
|
||||
sold out. If a voucher is used to apply a discount, the price of the purchased product is reduced by the
|
||||
discounted amount. Vouchers are connected to a specific event.
|
||||
* - | |:gb:| **Gift card**
|
||||
| |:de:| Geschenkgutschein
|
||||
- A :ref:`gift card <giftcards>` is a coupon representing an exact amount of money that can be used for purchases
|
||||
of any kind. Gift cards can be sold, created manually, or used as a method to refund your customer without paying
|
||||
them back directly.
|
||||
Unlike a voucher, it does not reduce the price of the purchased products when redeemed, but instead works as a
|
||||
payment method to lower the amount that needs to be paid through other methods. Gift cards are specific to an
|
||||
organizer by default but can even by shared between organizers.
|
||||
* - | |:gb:| **Cart**
|
||||
| |:de:| Warenkorb
|
||||
- A cart is a collection of products that are reserved by a customer who is currently completing the checkout
|
||||
process, but has not yet finished it.
|
||||
* - | |:gb:| **Order**
|
||||
| |:de:| Bestellung
|
||||
- An order is a purchase by a client, containing multiple different products. An order goes through various
|
||||
states and can change during its lifetime.
|
||||
* - | |:gb:| **Order code**
|
||||
| |:de:| Bestellnummer
|
||||
- An order code is the unique identifier of an order, usually consisting of 5 numbers and letters.
|
||||
* - | |:gb:| **Order position**
|
||||
| |:de:| Bestellposition
|
||||
- An order position is a single line inside an order, representing the purchase of one specific product. If the
|
||||
product is an admission product, this represents an attendee.
|
||||
* - | |:gb:| **Attendees**
|
||||
| |:de:| Teilnehmende
|
||||
- An attendee is the person designated to use a specific order position to access the event.
|
||||
* - | |:gb:| **Fee**
|
||||
| |:de:| Gebühr
|
||||
- A fee is an additional type of line inside an order that represents a cost that needs to be paid by the customer,
|
||||
but is not related to a specific product. A typical example is a shipping fee.
|
||||
* - | |:gb:| **Invoice** and **Cancellation**
|
||||
| |:de:| Rechnung und Rechnungskorrektur
|
||||
- An invoice refers to a legal document created to document a purchase for tax purposes. Invoices have individual
|
||||
numbers and no longer change after they have been issued. Every invoice is connected to an order, but an order
|
||||
can have multiple invoices: If an order changes, a cancellation document is created for the old invoice and a
|
||||
new invoice is created.
|
||||
* - | |:gb:| **Check-in**
|
||||
| |:de:| Check-in
|
||||
- A check-in is the event of someone being successfully scanned at an entry or exit of the event.
|
||||
* - | |:gb:| **Check-in list**
|
||||
| |:de:| Check-in-Liste
|
||||
- A check-in list is used to configure who can be scanned at a specific entry or exit of the event. Check-in lists
|
||||
are isolated from each other, so by default each ticket is valid once on every check-in list individually. They
|
||||
are therefore often used to represent *parts* of an event, either time-wise (e.g. conference days) or space-wise
|
||||
(e.g. rooms).
|
||||
* - | |:gb:| **Plugin**
|
||||
| |:de:| Erweiterung
|
||||
- A plugin is an optional software module that contains additional functionality and can be turned on and off per
|
||||
event. If you host pretix on your own server, most plugins need to be installed separately.
|
||||
* - | |:gb:| **Tax rule**
|
||||
| |:de:| Steuer-Regel
|
||||
- A tax rule defines how sales taxes are calculated for a product, possibly depending on type and country of the
|
||||
customer.
|
||||
* - | |:gb:| **Ticket**
|
||||
| |:de:| Ticket
|
||||
- A ticket usually refers to the actual file presented to the customer to be used at check-in, i.e. the PDF or
|
||||
Passbook file carrying the QR code. In some cases, "ticket" may also be used to refer to an order position,
|
||||
especially in case of admission products.
|
||||
* - | |:gb:| **Ticket secret**
|
||||
| |:de:| Ticket-Code
|
||||
- The ticket secret (sometimes "ticket code") is what's contained in the QR code on the ticket.
|
||||
* - | |:gb:| **Badge**
|
||||
| |:de:| Badge
|
||||
- A badge refers to the file used as a name tag for an attendee of your event.
|
||||
* - | |:gb:| **User**
|
||||
| |:de:| Benutzer
|
||||
- A user is anyone who can sign into the backend interface of pretix.
|
||||
* - | |:gb:| **Team**
|
||||
| |:de:| Team
|
||||
- A :ref:`team <user-teams>` is a collection of users who are granted some level of access to a set of events.
|
||||
* - | |:gb:| **Device**
|
||||
| |:de:| Gerät
|
||||
- A device is something that talks to pretix but does not run on a server. Usually a device refers to an
|
||||
installation of pretixSCAN, pretixPOS or some compatible third-party app on one of your computing devices.
|
||||
* - | |:gb:| **Gate**
|
||||
| |:de:| Station
|
||||
- A gate is a location at your event where people are being scanned, e.g. an entry or exit door. You can configure
|
||||
gates in pretix to group multiple devices together that are used in the same location, mostly for statistical
|
||||
purposes.
|
||||
* - | |:gb:| **Widget**
|
||||
| |:de:| Widget
|
||||
- The :ref:`widget` is a JavaScript component that can be used to embed the shop of an event or a list of events
|
||||
into a third-party web page.
|
||||
* - | |:gb:| **Sales channel**
|
||||
| |:de:| Verkaufskanal
|
||||
- A sales channel refers to the type in which a purchase arrived in the system, e.g. through pretix' web shop itself,
|
||||
or through other channels like box office or reseller sales.
|
||||
* - | |:gb:| **Box office**
|
||||
| |:de:| Abendkasse
|
||||
- Box office purchases refer to all purchases made in-person from the organizer directly, through a point of sale
|
||||
system like pretixPOS.
|
||||
* - | |:gb:| **Reseller**
|
||||
| |:de:| Vorverkaufsstelle
|
||||
- Resellers are third-party entities offering in-person sales of events to customers.
|
||||
@@ -15,4 +15,3 @@ wanting to use pretix to sell tickets.
|
||||
events/giftcards
|
||||
faq
|
||||
markdown
|
||||
glossary
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
include LICENSE
|
||||
include README.rst
|
||||
global-include *.proto
|
||||
recursive-include pretix/static *
|
||||
recursive-include pretix/static.dist *
|
||||
recursive-include pretix/locale *
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "3.13.1"
|
||||
__version__ = "3.12.0.dev0"
|
||||
|
||||
@@ -3,7 +3,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
class FullAccessSecurityProfile:
|
||||
identifier = 'full'
|
||||
verbose_name = _('Full device access (reading and changing orders and gift cards, reading of products and settings)')
|
||||
verbose_name = _('Full access')
|
||||
|
||||
def is_allowed(self, request):
|
||||
return True
|
||||
@@ -22,10 +22,8 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
|
||||
verbose_name = _('pretixSCAN')
|
||||
allowlist = (
|
||||
('GET', 'api-v1:version'),
|
||||
('GET', 'api-v1:device.eventselection'),
|
||||
('POST', 'api-v1:device.update'),
|
||||
('POST', 'api-v1:device.revoke'),
|
||||
('POST', 'api-v1:device.roll'),
|
||||
('GET', 'api-v1:device.update'),
|
||||
('GET', 'api-v1:device.revoke'),
|
||||
('GET', 'api-v1:event-list'),
|
||||
('GET', 'api-v1:event-detail'),
|
||||
('GET', 'api-v1:subevent-list'),
|
||||
@@ -39,7 +37,6 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'api-v1:checkinlist-status'),
|
||||
('GET', 'api-v1:checkinlistpos-list'),
|
||||
('POST', 'api-v1:checkinlistpos-redeem'),
|
||||
('GET', 'api-v1:revokedsecrets-list'),
|
||||
('GET', 'api-v1:order-list'),
|
||||
('GET', 'api-v1:event.settings'),
|
||||
)
|
||||
@@ -50,10 +47,8 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
|
||||
verbose_name = _('pretixSCAN (kiosk mode, online only)')
|
||||
allowlist = (
|
||||
('GET', 'api-v1:version'),
|
||||
('GET', 'api-v1:device.eventselection'),
|
||||
('POST', 'api-v1:device.update'),
|
||||
('POST', 'api-v1:device.revoke'),
|
||||
('POST', 'api-v1:device.roll'),
|
||||
('GET', 'api-v1:device.update'),
|
||||
('GET', 'api-v1:device.revoke'),
|
||||
('GET', 'api-v1:event-list'),
|
||||
('GET', 'api-v1:event-detail'),
|
||||
('GET', 'api-v1:subevent-list'),
|
||||
@@ -66,7 +61,6 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'api-v1:checkinlist-list'),
|
||||
('GET', 'api-v1:checkinlist-status'),
|
||||
('POST', 'api-v1:checkinlistpos-redeem'),
|
||||
('GET', 'api-v1:revokedsecrets-list'),
|
||||
('GET', 'api-v1:event.settings'),
|
||||
)
|
||||
|
||||
@@ -76,10 +70,8 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
|
||||
verbose_name = _('pretixPOS')
|
||||
allowlist = (
|
||||
('GET', 'api-v1:version'),
|
||||
('GET', 'api-v1:device.eventselection'),
|
||||
('POST', 'api-v1:device.update'),
|
||||
('POST', 'api-v1:device.revoke'),
|
||||
('POST', 'api-v1:device.roll'),
|
||||
('GET', 'api-v1:device.update'),
|
||||
('GET', 'api-v1:device.revoke'),
|
||||
('GET', 'api-v1:event-list'),
|
||||
('GET', 'api-v1:event-detail'),
|
||||
('GET', 'api-v1:subevent-list'),
|
||||
@@ -106,12 +98,7 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
|
||||
('POST', 'plugins:pretix_posbackend:posclosing-list'),
|
||||
('POST', 'plugins:pretix_posbackend:posdebugdump-list'),
|
||||
('POST', 'plugins:pretix_posbackend:stripeterminal.token'),
|
||||
('GET', 'api-v1:revokedsecrets-list'),
|
||||
('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'),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ class CheckinListSerializer(I18nAwareModelSerializer):
|
||||
model = CheckinList
|
||||
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count',
|
||||
'include_pending', 'auto_checkin_sales_channels', 'allow_multiple_entries', 'allow_entry_after_exit',
|
||||
'rules', 'exit_all_at')
|
||||
'rules')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -95,41 +95,19 @@ class TimeZoneField(ChoiceField):
|
||||
)
|
||||
|
||||
|
||||
class ValidKeysField(Field):
|
||||
def to_representation(self, value):
|
||||
return value.cache.get_or_set(
|
||||
'ticket_secret_valid_keys',
|
||||
lambda: self._get(value),
|
||||
120
|
||||
)
|
||||
|
||||
def _get(self, value):
|
||||
return {
|
||||
'pretix_sig1': [
|
||||
value.settings.ticket_secrets_pretix_sig1_pubkey
|
||||
] if value.settings.ticket_secrets_pretix_sig1_pubkey else []
|
||||
}
|
||||
|
||||
|
||||
class EventSerializer(I18nAwareModelSerializer):
|
||||
meta_data = MetaDataField(required=False, source='*')
|
||||
item_meta_properties = MetaPropertyField(required=False, source='*')
|
||||
plugins = PluginsField(required=False, source='*')
|
||||
seat_category_mapping = SeatCategoryMappingField(source='*', required=False)
|
||||
timezone = TimeZoneField(required=False, choices=[(a, a) for a in common_timezones])
|
||||
valid_keys = ValidKeysField(source='*', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Event
|
||||
fields = ('name', 'slug', 'live', 'testmode', 'currency', 'date_from',
|
||||
'date_to', 'date_admission', 'is_public', 'presale_start',
|
||||
'presale_end', 'location', 'geo_lat', 'geo_lon', 'has_subevents', 'meta_data', 'seating_plan',
|
||||
'plugins', 'seat_category_mapping', 'timezone', 'item_meta_properties', 'valid_keys')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if not hasattr(self.context['request'], 'event'):
|
||||
self.fields.pop('valid_keys')
|
||||
'plugins', 'seat_category_mapping', 'timezone', 'item_meta_properties')
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
@@ -391,7 +369,7 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
||||
fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission',
|
||||
'presale_start', 'presale_end', 'location', 'geo_lat', 'geo_lon', 'event', 'is_public',
|
||||
'seating_plan', 'item_price_overrides', 'variation_price_overrides', 'meta_data',
|
||||
'seat_category_mapping', 'last_modified')
|
||||
'seat_category_mapping')
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
@@ -611,7 +589,6 @@ class EventSettingsSerializer(serializers.Serializer):
|
||||
'ticket_download_addons',
|
||||
'ticket_download_nonadm',
|
||||
'ticket_download_pending',
|
||||
'ticket_download_require_validated_email',
|
||||
'mail_prefix',
|
||||
'mail_from',
|
||||
'mail_from_name',
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
from django import forms
|
||||
from django.http import QueryDict
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class FormFieldWrapperField(serializers.Field):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.form_field = kwargs.pop('form_field')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def to_representation(self, value):
|
||||
return self.form_field.widget.format_value(value)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
d = self.form_field.widget.value_from_datadict({'name': data}, {}, 'name')
|
||||
d = self.form_field.clean(d)
|
||||
return d
|
||||
|
||||
|
||||
simple_mappings = (
|
||||
(forms.DateField, serializers.DateField, tuple()),
|
||||
(forms.TimeField, serializers.TimeField, tuple()),
|
||||
(forms.SplitDateTimeField, serializers.DateTimeField, tuple()),
|
||||
(forms.DateTimeField, serializers.DateTimeField, tuple()),
|
||||
(forms.DecimalField, serializers.DecimalField, ('max_digits', 'decimal_places', 'min_value', 'max_value')),
|
||||
(forms.FloatField, serializers.FloatField, tuple()),
|
||||
(forms.IntegerField, serializers.IntegerField, tuple()),
|
||||
(forms.EmailField, serializers.EmailField, tuple()),
|
||||
(forms.UUIDField, serializers.UUIDField, tuple()),
|
||||
(forms.URLField, serializers.URLField, tuple()),
|
||||
(forms.NullBooleanField, serializers.NullBooleanField, tuple()),
|
||||
(forms.BooleanField, serializers.BooleanField, tuple()),
|
||||
)
|
||||
|
||||
|
||||
class SerializerDescriptionField(serializers.Field):
|
||||
def to_representation(self, value):
|
||||
fields = []
|
||||
for k, v in value.fields.items():
|
||||
d = {
|
||||
'name': k,
|
||||
'required': v.required,
|
||||
}
|
||||
if isinstance(v, serializers.ChoiceField):
|
||||
d['choices'] = list(v.choices.keys())
|
||||
fields.append(d)
|
||||
|
||||
return fields
|
||||
|
||||
|
||||
class ExporterSerializer(serializers.Serializer):
|
||||
identifier = serializers.CharField()
|
||||
verbose_name = serializers.CharField()
|
||||
input_parameters = SerializerDescriptionField(source='_serializer')
|
||||
|
||||
|
||||
class PrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
|
||||
def to_representation(self, value):
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
return super().to_representation(value)
|
||||
|
||||
|
||||
class JobRunSerializer(serializers.Serializer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
ex = kwargs.pop('exporter')
|
||||
events = kwargs.pop('events', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
if events is not None:
|
||||
self.fields["events"] = serializers.SlugRelatedField(
|
||||
queryset=events,
|
||||
required=True,
|
||||
allow_empty=False,
|
||||
slug_field='slug',
|
||||
many=True
|
||||
)
|
||||
for k, v in ex.export_form_fields.items():
|
||||
for m_from, m_to, m_kwargs in simple_mappings:
|
||||
if isinstance(v, m_from):
|
||||
self.fields[k] = m_to(
|
||||
required=v.required,
|
||||
allow_null=not v.required,
|
||||
validators=v.validators,
|
||||
**{kwarg: getattr(v, kwargs, None) for kwarg in m_kwargs}
|
||||
)
|
||||
break
|
||||
|
||||
if isinstance(v, forms.ModelMultipleChoiceField):
|
||||
self.fields[k] = PrimaryKeyRelatedField(
|
||||
queryset=v.queryset,
|
||||
required=v.required,
|
||||
allow_empty=not v.required,
|
||||
validators=v.validators,
|
||||
many=True
|
||||
)
|
||||
elif isinstance(v, forms.ModelChoiceField):
|
||||
self.fields[k] = PrimaryKeyRelatedField(
|
||||
queryset=v.queryset,
|
||||
required=v.required,
|
||||
allow_null=not v.required,
|
||||
validators=v.validators,
|
||||
)
|
||||
elif isinstance(v, forms.MultipleChoiceField):
|
||||
self.fields[k] = serializers.MultipleChoiceField(
|
||||
choices=v.choices,
|
||||
required=v.required,
|
||||
allow_empty=not v.required,
|
||||
validators=v.validators,
|
||||
)
|
||||
elif isinstance(v, forms.ChoiceField):
|
||||
self.fields[k] = serializers.ChoiceField(
|
||||
choices=v.choices,
|
||||
required=v.required,
|
||||
allow_null=not v.required,
|
||||
validators=v.validators,
|
||||
)
|
||||
else:
|
||||
self.fields[k] = FormFieldWrapperField(form_field=v, required=v.required, allow_null=not v.required)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if isinstance(data, QueryDict):
|
||||
data = data.copy()
|
||||
for k, v in self.fields.items():
|
||||
if isinstance(v, serializers.ManyRelatedField) and k not in data:
|
||||
data[k] = []
|
||||
data = super().to_internal_value(data)
|
||||
return data
|
||||
@@ -21,7 +21,7 @@ from pretix.base.models import (
|
||||
OrderPosition, Question, QuestionAnswer, Seat, SubEvent, TaxRule, Voucher,
|
||||
)
|
||||
from pretix.base.models.orders import (
|
||||
CartPosition, OrderFee, OrderPayment, OrderRefund, RevokedTicketSecret,
|
||||
CartPosition, OrderFee, OrderPayment, OrderRefund,
|
||||
)
|
||||
from pretix.base.pdf import get_variables
|
||||
from pretix.base.services.cart import error_messages
|
||||
@@ -1209,10 +1209,3 @@ class OrderRefundCreateSerializer(I18nAwareModelSerializer):
|
||||
order = OrderRefund(order=self.context['order'], payment=p, **validated_data)
|
||||
order.save()
|
||||
return order
|
||||
|
||||
|
||||
class RevokedTicketSecretSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = RevokedTicketSecret
|
||||
fields = ('id', 'secret', 'created')
|
||||
|
||||
@@ -7,8 +7,8 @@ from rest_framework import routers
|
||||
from pretix.api.views import cart
|
||||
|
||||
from .views import (
|
||||
checkin, device, event, exporters, item, oauth, order, organizer, user,
|
||||
version, voucher, waitinglist, webhooks,
|
||||
checkin, device, event, item, oauth, order, organizer, user, version,
|
||||
voucher, waitinglist, webhooks,
|
||||
)
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
@@ -22,7 +22,6 @@ orga_router.register(r'seatingplans', organizer.SeatingPlanViewSet)
|
||||
orga_router.register(r'giftcards', organizer.GiftCardViewSet)
|
||||
orga_router.register(r'teams', organizer.TeamViewSet)
|
||||
orga_router.register(r'devices', organizer.DeviceViewSet)
|
||||
orga_router.register(r'exporters', exporters.OrganizerExportersViewSet, basename='exporters')
|
||||
|
||||
team_router = routers.DefaultRouter()
|
||||
team_router.register(r'members', organizer.TeamMemberViewSet)
|
||||
@@ -40,12 +39,10 @@ event_router.register(r'vouchers', voucher.VoucherViewSet)
|
||||
event_router.register(r'orders', order.OrderViewSet)
|
||||
event_router.register(r'orderpositions', order.OrderPositionViewSet)
|
||||
event_router.register(r'invoices', order.InvoiceViewSet)
|
||||
event_router.register(r'revokedsecrets', order.RevokedSecretViewSet, basename='revokedsecrets')
|
||||
event_router.register(r'taxrules', event.TaxRuleViewSet)
|
||||
event_router.register(r'waitinglistentries', waitinglist.WaitingListViewSet)
|
||||
event_router.register(r'checkinlists', checkin.CheckinListViewSet)
|
||||
event_router.register(r'cartpositions', cart.CartPositionViewSet)
|
||||
event_router.register(r'exporters', exporters.EventExportersViewSet, basename='exporters')
|
||||
|
||||
checkinlist_router = routers.DefaultRouter()
|
||||
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet, basename='checkinlistpos')
|
||||
@@ -88,7 +85,6 @@ urlpatterns = [
|
||||
url(r"^device/update$", device.UpdateView.as_view(), name="device.update"),
|
||||
url(r"^device/roll$", device.RollKeyView.as_view(), name="device.roll"),
|
||||
url(r"^device/revoke$", device.RevokeKeyView.as_view(), name="device.revoke"),
|
||||
url(r"^device/eventselection$", device.EventSelectionView.as_view(), name="device.eventselection"),
|
||||
url(r"^me$", user.MeView.as_view(), name="user.me"),
|
||||
url(r"^version$", version.VersionView.as_view(), name="version"),
|
||||
]
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import django_filters
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import (
|
||||
Count, Exists, F, Max, OuterRef, Prefetch, Q, Subquery,
|
||||
)
|
||||
from django.db.models import Count, F, Max, OuterRef, Prefetch, Q, Subquery
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
@@ -92,12 +90,20 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
def status(self, *args, **kwargs):
|
||||
with language(self.request.event.settings.locale):
|
||||
clist = self.get_object()
|
||||
cqs = clist.positions.annotate(
|
||||
checkedin=Exists(Checkin.objects.filter(list_id=clist.pk, position=OuterRef('pk'), type=Checkin.TYPE_ENTRY))
|
||||
).filter(
|
||||
checkedin=True,
|
||||
cqs = Checkin.objects.filter(
|
||||
position__order__event=clist.event,
|
||||
position__order__status__in=[Order.STATUS_PAID] + ([Order.STATUS_PENDING] if clist.include_pending else []),
|
||||
list=clist
|
||||
)
|
||||
pqs = clist.positions
|
||||
pqs = OrderPosition.objects.filter(
|
||||
order__event=clist.event,
|
||||
order__status__in=[Order.STATUS_PAID] + ([Order.STATUS_PENDING] if clist.include_pending else []),
|
||||
)
|
||||
if clist.subevent:
|
||||
pqs = pqs.filter(subevent=clist.subevent)
|
||||
if not clist.all_products:
|
||||
pqs = pqs.filter(item__in=clist.limit_products.values_list('id', flat=True))
|
||||
cqs = cqs.filter(position__item__in=clist.limit_products.values_list('id', flat=True))
|
||||
|
||||
ev = clist.subevent or clist.event
|
||||
response = {
|
||||
@@ -105,8 +111,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
'name': str(ev.name),
|
||||
},
|
||||
'checkin_count': cqs.count(),
|
||||
'position_count': pqs.count(),
|
||||
'inside_count': clist.inside_count,
|
||||
'position_count': pqs.count()
|
||||
}
|
||||
|
||||
op_by_item = {
|
||||
@@ -118,12 +123,12 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
for p in pqs.order_by().values('variation').annotate(cnt=Count('id'))
|
||||
}
|
||||
c_by_item = {
|
||||
p['item']: p['cnt']
|
||||
for p in cqs.order_by().values('item').annotate(cnt=Count('id'))
|
||||
p['position__item']: p['cnt']
|
||||
for p in cqs.order_by().values('position__item').annotate(cnt=Count('id'))
|
||||
}
|
||||
c_by_variation = {
|
||||
p['variation']: p['cnt']
|
||||
for p in cqs.order_by().values('variation').annotate(cnt=Count('id'))
|
||||
p['position__variation']: p['cnt']
|
||||
for p in cqs.order_by().values('position__variation').annotate(cnt=Count('id'))
|
||||
}
|
||||
|
||||
if not clist.all_products:
|
||||
@@ -257,7 +262,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
|
||||
return qs
|
||||
|
||||
@action(detail=False, methods=['POST'], url_name='redeem', url_path='(?P<pk>.*)/redeem')
|
||||
@action(detail=False, methods=['POST'], url_name='redeem', url_path='(?P<pk>[^/]+)/redeem')
|
||||
def redeem(self, *args, **kwargs):
|
||||
force = bool(self.request.data.get('force', False))
|
||||
type = self.request.data.get('type', None) or Checkin.TYPE_ENTRY
|
||||
@@ -278,23 +283,13 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
else:
|
||||
op = queryset.get(secret=self.kwargs['pk'])
|
||||
except OrderPosition.DoesNotExist:
|
||||
revoked_matches = list(self.request.event.revoked_secrets.filter(secret=self.kwargs['pk']))
|
||||
if len(revoked_matches) == 0 or not force:
|
||||
self.request.event.log_action('pretix.event.checkin.unknown', data={
|
||||
'datetime': dt,
|
||||
'type': type,
|
||||
'list': self.checkinlist.pk,
|
||||
'barcode': self.kwargs['pk']
|
||||
}, user=self.request.user, auth=self.request.auth)
|
||||
raise Http404()
|
||||
|
||||
op = revoked_matches[0].position
|
||||
op.order.log_action('pretix.event.checkin.revoked', data={
|
||||
self.request.event.log_action('pretix.event.checkin.unknown', data={
|
||||
'datetime': dt,
|
||||
'type': type,
|
||||
'list': self.checkinlist.pk,
|
||||
'barcode': self.kwargs['pk']
|
||||
}, user=self.request.user, auth=self.request.auth)
|
||||
raise Http404()
|
||||
|
||||
given_answers = {}
|
||||
if 'answers' in self.request.data:
|
||||
@@ -335,7 +330,6 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
'position': op.id,
|
||||
'positionid': op.positionid,
|
||||
'errorcode': e.code,
|
||||
'force': force,
|
||||
'datetime': dt,
|
||||
'type': type,
|
||||
'list': self.checkinlist.pk
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import logging
|
||||
|
||||
from django.db.models import Exists, OuterRef, Q
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils.timezone import now
|
||||
from rest_framework import serializers, status
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from pretix.api.auth.device import DeviceTokenAuthentication
|
||||
from pretix.base.models import CheckinList, Device, SubEvent
|
||||
from pretix.base.models.devices import Gate, generate_api_token
|
||||
from pretix.base.models import Device
|
||||
from pretix.base.models.devices import generate_api_token
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -30,25 +28,14 @@ class UpdateRequestSerializer(serializers.Serializer):
|
||||
software_version = serializers.CharField(max_length=190)
|
||||
|
||||
|
||||
class GateSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Gate
|
||||
fields = [
|
||||
'id',
|
||||
'name',
|
||||
'identifier',
|
||||
]
|
||||
|
||||
|
||||
class DeviceSerializer(serializers.ModelSerializer):
|
||||
organizer = serializers.SlugRelatedField(slug_field='slug', read_only=True)
|
||||
gate = GateSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = [
|
||||
'organizer', 'device_id', 'unique_serial', 'api_token',
|
||||
'name', 'security_profile', 'gate'
|
||||
'name', 'security_profile'
|
||||
]
|
||||
|
||||
|
||||
@@ -124,156 +111,3 @@ class RevokeKeyView(APIView):
|
||||
|
||||
serializer = DeviceSerializer(device)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class EventSelectionView(APIView):
|
||||
authentication_classes = (DeviceTokenAuthentication,)
|
||||
|
||||
@property
|
||||
def base_event_qs(self):
|
||||
qs = self.request.auth.organizer.events.annotate(
|
||||
first_date=Coalesce('date_admission', 'date_from'),
|
||||
last_date=Coalesce('date_to', 'date_from'),
|
||||
).filter(
|
||||
live=True,
|
||||
has_subevents=False
|
||||
).order_by('first_date')
|
||||
if self.request.auth.gate:
|
||||
has_cl = CheckinList.objects.filter(
|
||||
event=OuterRef('pk'),
|
||||
gates__in=[self.request.auth.gate]
|
||||
)
|
||||
qs = qs.annotate(has_cl=Exists(has_cl)).filter(has_cl=True)
|
||||
return qs
|
||||
|
||||
@property
|
||||
def base_subevent_qs(self):
|
||||
qs = SubEvent.objects.annotate(
|
||||
first_date=Coalesce('date_admission', 'date_from'),
|
||||
last_date=Coalesce('date_to', 'date_from'),
|
||||
).filter(
|
||||
event__organizer=self.request.auth.organizer,
|
||||
event__live=True,
|
||||
active=True,
|
||||
).select_related('event').order_by('first_date')
|
||||
if self.request.auth.gate:
|
||||
has_cl = CheckinList.objects.filter(
|
||||
Q(subevent__isnull=True) | Q(subevent=OuterRef('pk')),
|
||||
event_id=OuterRef('event_id'),
|
||||
gates__in=[self.request.auth.gate]
|
||||
)
|
||||
qs = qs.annotate(has_cl=Exists(has_cl)).filter(has_cl=True)
|
||||
return qs
|
||||
|
||||
def get(self, request, format=None):
|
||||
device = request.auth
|
||||
current_event = None
|
||||
current_subevent = None
|
||||
if 'current_event' in request.query_params:
|
||||
current_event = device.organizer.events.filter(slug=request.query_params['current_event']).first()
|
||||
if current_event and 'current_subevent' in request.query_params:
|
||||
current_subevent = current_event.subevents.filter(pk=request.query_params['current_subevent']).first()
|
||||
if current_event and current_event.has_subevents and not current_subevent:
|
||||
current_event = None
|
||||
|
||||
if current_event:
|
||||
current_ev = current_subevent or current_event
|
||||
current_ev_start = current_ev.date_admission or current_ev.date_from
|
||||
tz = current_event.timezone
|
||||
if current_ev.date_to and current_ev_start < now() < current_ev.date_to:
|
||||
# The event that is selected is currently running. Good enough.
|
||||
return Response(status=status.HTTP_304_NOT_MODIFIED)
|
||||
|
||||
# The event that is selected is not currently running. We cannot rely on all events having a proper end date.
|
||||
# In any case, we'll need to decide between the event that last started (and might still be running) and the
|
||||
# event that starts next (and might already be letting people in), so let's get these two!
|
||||
last_started_ev = self.base_event_qs.filter(first_date__lte=now()).last() or self.base_subevent_qs.filter(
|
||||
first_date__lte=now()).last()
|
||||
|
||||
upcoming_event = self.base_event_qs.filter(first_date__gt=now()).first()
|
||||
upcoming_subevent = self.base_subevent_qs.filter(first_date__gt=now()).first()
|
||||
if upcoming_event and upcoming_subevent:
|
||||
if upcoming_event.first_date > upcoming_subevent.first_date:
|
||||
upcoming_ev = upcoming_subevent
|
||||
else:
|
||||
upcoming_ev = upcoming_event
|
||||
else:
|
||||
upcoming_ev = upcoming_event or upcoming_subevent
|
||||
|
||||
if not upcoming_ev and not last_started_ev:
|
||||
# Ooops, no events here
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
elif upcoming_ev and not last_started_ev:
|
||||
# No event running, so let's take the next one
|
||||
return self._suggest_event(current_event, upcoming_ev)
|
||||
elif last_started_ev and not upcoming_ev:
|
||||
# No event upcoming, so let's take the next one
|
||||
return self._suggest_event(current_event, last_started_ev)
|
||||
|
||||
if last_started_ev.date_to and now() < last_started_ev.date_to:
|
||||
# The event that last started is currently running. Good enough.
|
||||
return self._suggest_event(current_event, last_started_ev)
|
||||
|
||||
if not current_event:
|
||||
tz = (upcoming_event or last_started_ev).timezone
|
||||
|
||||
lse_d = last_started_ev.date_from.astimezone(tz).date()
|
||||
upc_d = upcoming_ev.date_from.astimezone(tz).date()
|
||||
now_d = now().astimezone(tz).date()
|
||||
if lse_d == now_d and upc_d != now_d:
|
||||
# Last event was today, next is tomorrow, stick with today
|
||||
return self._suggest_event(current_event, last_started_ev)
|
||||
elif lse_d != now_d and upc_d == now_d:
|
||||
# Last event was yesterday, next is today, stick with today
|
||||
return self._suggest_event(current_event, upcoming_ev)
|
||||
|
||||
# Both last and next event are today, we switch over in the middle
|
||||
if now() > last_started_ev.last_date + (upcoming_ev.first_date - last_started_ev.last_date) / 2:
|
||||
return self._suggest_event(current_event, upcoming_ev)
|
||||
else:
|
||||
return self._suggest_event(current_event, last_started_ev)
|
||||
|
||||
def _suggest_event(self, current_event, ev):
|
||||
current_checkinlist = None
|
||||
if current_event and 'current_checkinlist' in self.request.query_params:
|
||||
current_checkinlist = current_event.checkin_lists.filter(
|
||||
pk=self.request.query_params['current_checkinlist']
|
||||
).first()
|
||||
if isinstance(ev, SubEvent):
|
||||
checkinlist_qs = ev.event.checkin_lists.filter(Q(subevent__isnull=True) | Q(subevent=ev))
|
||||
else:
|
||||
checkinlist_qs = ev.checkin_lists
|
||||
|
||||
if self.request.auth.gate:
|
||||
checkinlist_qs = checkinlist_qs.filter(gates__in=[self.request.auth.gate])
|
||||
|
||||
checkinlist = None
|
||||
if current_checkinlist:
|
||||
checkinlist = checkinlist_qs.filter(Q(name=current_checkinlist.name) | Q(pk=current_checkinlist.pk)).first()
|
||||
if not checkinlist:
|
||||
checkinlist = checkinlist_qs.first()
|
||||
r = {
|
||||
'event': {
|
||||
'slug': ev.event.slug if isinstance(ev, SubEvent) else ev.slug,
|
||||
'name': str(ev.event.name) if isinstance(ev, SubEvent) else str(ev.name),
|
||||
},
|
||||
'subevent': ev.pk if isinstance(ev, SubEvent) else None,
|
||||
'checkinlist': checkinlist.pk if checkinlist else None,
|
||||
}
|
||||
|
||||
if r == {
|
||||
'event': {
|
||||
'slug': current_event.slug if current_event else None,
|
||||
'name': str(current_event.name) if current_event else None,
|
||||
},
|
||||
'subevent': (
|
||||
int(self.request.query_params.get('current_subevent'))
|
||||
if self.request.query_params.get('current_subevent') else None
|
||||
),
|
||||
'checkinlist': (
|
||||
int(self.request.query_params.get('current_checkinlist'))
|
||||
if self.request.query_params.get('current_checkinlist') else None
|
||||
),
|
||||
}:
|
||||
return Response(status=status.HTTP_304_NOT_MODIFIED)
|
||||
return Response(r)
|
||||
|
||||
@@ -4,7 +4,7 @@ from django.db.models import ProtectedError, Q
|
||||
from django.utils.timezone import now
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import filters, serializers, views, viewsets
|
||||
from rest_framework import filters, views, viewsets
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.response import Response
|
||||
|
||||
@@ -89,6 +89,7 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
|
||||
qs = filter_qs_by_attr(qs, self.request)
|
||||
|
||||
return qs.prefetch_related(
|
||||
'meta_values', 'meta_values__property', 'seat_category_mappings'
|
||||
)
|
||||
@@ -193,7 +194,6 @@ with scopes_disabled():
|
||||
is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs')
|
||||
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
|
||||
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
|
||||
modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte')
|
||||
|
||||
class Meta:
|
||||
model = SubEvent
|
||||
@@ -233,8 +233,6 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
write_permission = 'can_change_event_settings'
|
||||
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
|
||||
filterset_class = SubEventFilter
|
||||
ordering = ('date_from',)
|
||||
ordering_fields = ('id', 'date_from', 'last_modified')
|
||||
|
||||
def get_queryset(self):
|
||||
if getattr(self.request, 'event', None):
|
||||
@@ -256,20 +254,6 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
'subeventitem_set', 'subeventitemvariation_set', 'seat_category_mappings'
|
||||
)
|
||||
|
||||
def list(self, request, **kwargs):
|
||||
date = serializers.DateTimeField().to_representation(now())
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
resp = self.get_paginated_response(serializer.data)
|
||||
resp['X-Page-Generated'] = date
|
||||
return resp
|
||||
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data, headers={'X-Page-Generated': date})
|
||||
|
||||
def perform_update(self, serializer):
|
||||
original_data = self.get_serializer(instance=serializer.instance).data
|
||||
super().perform_update(serializer)
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from celery.result import AsyncResult
|
||||
from django.conf import settings
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from pretix.api.serializers.exporters import (
|
||||
ExporterSerializer, JobRunSerializer,
|
||||
)
|
||||
from pretix.base.models import CachedFile, Device, TeamAPIToken
|
||||
from pretix.base.services.export import export, multiexport
|
||||
from pretix.base.signals import (
|
||||
register_data_exporters, register_multievent_data_exporters,
|
||||
)
|
||||
from pretix.helpers.http import ChunkBasedFileResponse
|
||||
|
||||
|
||||
class ExportersMixin:
|
||||
def list(self, request, *args, **kwargs):
|
||||
res = ExporterSerializer(self.exporters, many=True)
|
||||
return Response({
|
||||
"count": len(self.exporters),
|
||||
"next": None,
|
||||
"previous": None,
|
||||
"results": res.data
|
||||
})
|
||||
|
||||
def get_object(self):
|
||||
instances = [e for e in self.exporters if e.identifier == self.kwargs.get('pk')]
|
||||
if not instances:
|
||||
raise Http404()
|
||||
return instances[0]
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
serializer = ExporterSerializer(instance)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=['GET'], url_name='download', url_path='download/(?P<asyncid>[^/]+)/(?P<cfid>[^/]+)')
|
||||
def download(self, *args, **kwargs):
|
||||
cf = get_object_or_404(CachedFile, id=kwargs['cfid'])
|
||||
if cf.file:
|
||||
resp = ChunkBasedFileResponse(cf.file.file, content_type=cf.type)
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename)
|
||||
return resp
|
||||
elif not settings.HAS_CELERY:
|
||||
return Response(
|
||||
{'status': 'failed', 'message': 'Unknown file ID or export failed'},
|
||||
status=status.HTTP_410_GONE
|
||||
)
|
||||
|
||||
res = AsyncResult(kwargs['asyncid'])
|
||||
if res.failed():
|
||||
if isinstance(res.info, dict) and res.info['exc_type'] == 'ExportError':
|
||||
msg = res.info['exc_message']
|
||||
else:
|
||||
msg = 'Internal error'
|
||||
return Response(
|
||||
{'status': 'failed', 'message': msg},
|
||||
status=status.HTTP_410_GONE
|
||||
)
|
||||
|
||||
return Response(
|
||||
{
|
||||
'status': 'running' if res.state in ('PROGRESS', 'STARTED', 'SUCCESS') else 'waiting',
|
||||
'percentage': res.result.get('value', None) if res.result else None,
|
||||
},
|
||||
status=status.HTTP_409_CONFLICT
|
||||
)
|
||||
|
||||
@action(detail=True, methods=['POST'])
|
||||
def run(self, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
serializer = JobRunSerializer(exporter=instance, data=self.request.data, **self.get_serializer_kwargs())
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
cf = CachedFile(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
|
||||
})
|
||||
@@ -26,18 +26,15 @@ from pretix.api.serializers.order import (
|
||||
InvoiceSerializer, OrderCreateSerializer, OrderPaymentCreateSerializer,
|
||||
OrderPaymentSerializer, OrderPositionSerializer,
|
||||
OrderRefundCreateSerializer, OrderRefundSerializer, OrderSerializer,
|
||||
PriceCalcSerializer, RevokedTicketSecretSerializer,
|
||||
SimulatedOrderSerializer,
|
||||
PriceCalcSerializer, SimulatedOrderSerializer,
|
||||
)
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedCombinedTicket, CachedTicket, Device, Event, Invoice, InvoiceAddress,
|
||||
Order, OrderFee, OrderPayment, OrderPosition, OrderRefund, Quota, SubEvent,
|
||||
TaxRule, TeamAPIToken, generate_secret,
|
||||
TeamAPIToken, generate_position_secret, generate_secret,
|
||||
)
|
||||
from pretix.base.models.orders import RevokedTicketSecret
|
||||
from pretix.base.payment import PaymentException
|
||||
from pretix.base.secrets import assign_ticket_secret
|
||||
from pretix.base.services import tickets
|
||||
from pretix.base.services.invoices import (
|
||||
generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified,
|
||||
@@ -65,7 +62,6 @@ with scopes_disabled():
|
||||
modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte')
|
||||
created_since = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='gte')
|
||||
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')
|
||||
|
||||
class Meta:
|
||||
@@ -85,19 +81,6 @@ with scopes_disabled():
|
||||
).filter(has_se_after=True)
|
||||
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):
|
||||
u = value
|
||||
if "-" in value:
|
||||
@@ -500,9 +483,8 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
order = self.get_object()
|
||||
order.secret = generate_secret()
|
||||
for op in order.all_positions.all():
|
||||
assign_ticket_secret(
|
||||
request.event, op, force_invalidate=True, save=True
|
||||
)
|
||||
op.secret = generate_position_secret()
|
||||
op.save()
|
||||
order.save(update_fields=['secret'])
|
||||
CachedTicket.objects.filter(order_position__order=order).delete()
|
||||
CachedCombinedTicket.objects.filter(order=order).delete()
|
||||
@@ -561,10 +543,7 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
serializer = OrderCreateSerializer(data=request.data, context=self.get_serializer_context())
|
||||
serializer.is_valid(raise_exception=True)
|
||||
with transaction.atomic():
|
||||
try:
|
||||
self.perform_create(serializer)
|
||||
except TaxRule.SaleNotAllowed:
|
||||
raise ValidationError(_('One of the selected products is not available in the selected country.'))
|
||||
self.perform_create(serializer)
|
||||
send_mail = serializer._send_mail
|
||||
order = serializer.instance
|
||||
if not order.pk:
|
||||
@@ -1319,26 +1298,3 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
auth=self.request.auth,
|
||||
)
|
||||
return Response(status=204)
|
||||
|
||||
|
||||
with scopes_disabled():
|
||||
class RevokedSecretFilter(FilterSet):
|
||||
created_since = django_filters.IsoDateTimeFilter(field_name='created', lookup_expr='gte')
|
||||
|
||||
class Meta:
|
||||
model = RevokedTicketSecret
|
||||
fields = []
|
||||
|
||||
|
||||
class RevokedSecretViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = RevokedTicketSecretSerializer
|
||||
queryset = RevokedTicketSecret.objects.none()
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||
ordering = ('-created',)
|
||||
ordering_fields = ('created', 'secret')
|
||||
filterset_class = RevokedSecretFilter
|
||||
permission = 'can_view_orders'
|
||||
write_permission = 'can_change_orders'
|
||||
|
||||
def get_queryset(self):
|
||||
return RevokedTicketSecret.objects.filter(event=self.request.event)
|
||||
|
||||
@@ -7,7 +7,7 @@ import requests
|
||||
from celery.exceptions import MaxRetriesExceededError
|
||||
from django.db.models import Exists, OuterRef, Q
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes import scope, scopes_disabled
|
||||
from requests import RequestException
|
||||
|
||||
@@ -97,67 +97,6 @@ class ParametrizedOrderWebhookEvent(WebhookEvent):
|
||||
}
|
||||
|
||||
|
||||
class ParametrizedEventWebhookEvent(WebhookEvent):
|
||||
def __init__(self, action_type, verbose_name):
|
||||
self._action_type = action_type
|
||||
self._verbose_name = verbose_name
|
||||
super().__init__()
|
||||
|
||||
@property
|
||||
def action_type(self):
|
||||
return self._action_type
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
return self._verbose_name
|
||||
|
||||
def build_payload(self, logentry: LogEntry):
|
||||
if logentry.action_type == 'pretix.event.deleted':
|
||||
organizer = logentry.content_object
|
||||
return {
|
||||
'notification_id': logentry.pk,
|
||||
'organizer': organizer.slug,
|
||||
'event': logentry.parsed_data.get('slug'),
|
||||
'action': logentry.action_type,
|
||||
}
|
||||
|
||||
event = logentry.content_object
|
||||
if not event:
|
||||
return None
|
||||
|
||||
return {
|
||||
'notification_id': logentry.pk,
|
||||
'organizer': event.organizer.slug,
|
||||
'event': event.slug,
|
||||
'action': logentry.action_type,
|
||||
}
|
||||
|
||||
|
||||
class ParametrizedSubEventWebhookEvent(WebhookEvent):
|
||||
def __init__(self, action_type, verbose_name):
|
||||
self._action_type = action_type
|
||||
self._verbose_name = verbose_name
|
||||
super().__init__()
|
||||
|
||||
@property
|
||||
def action_type(self):
|
||||
return self._action_type
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
return self._verbose_name
|
||||
|
||||
def build_payload(self, logentry: LogEntry):
|
||||
# do not use content_object, this is also called in deletion
|
||||
return {
|
||||
'notification_id': logentry.pk,
|
||||
'organizer': logentry.event.organizer.slug,
|
||||
'event': logentry.event.slug,
|
||||
'subevent': logentry.object_id,
|
||||
'action': logentry.action_type,
|
||||
}
|
||||
|
||||
|
||||
class ParametrizedOrderPositionWebhookEvent(ParametrizedOrderWebhookEvent):
|
||||
|
||||
def build_payload(self, logentry: LogEntry):
|
||||
@@ -230,69 +169,44 @@ def register_default_webhook_events(sender, **kwargs):
|
||||
'pretix.event.checkin.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)
|
||||
def notify_webhooks(logentry_ids: list):
|
||||
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:
|
||||
break # We need to know the organizer
|
||||
def notify_webhooks(logentry_id: int):
|
||||
logentry = LogEntry.all.select_related('event', 'event__organizer').get(id=logentry_id)
|
||||
|
||||
notification_type = logentry.webhook_type
|
||||
if not logentry.organizer:
|
||||
return # We need to know the organizer
|
||||
|
||||
if not notification_type:
|
||||
break # Ignore, no webhooks for this event type
|
||||
types = get_all_webhook_events()
|
||||
notification_type = None
|
||||
typepath = logentry.action_type
|
||||
while not notification_type and '.' in typepath:
|
||||
notification_type = types.get(typepath + ('.*' if typepath != logentry.action_type else ''))
|
||||
typepath = typepath.rsplit('.', 1)[0]
|
||||
|
||||
if _org != logentry.organizer or _at != logentry.action_type or webhooks is None:
|
||||
_org = logentry.organizer
|
||||
_at = logentry.action_type
|
||||
if not notification_type:
|
||||
return # Ignore, no webhooks for this event type
|
||||
|
||||
# All webhooks that registered for this notification
|
||||
event_listener = WebHookEventListener.objects.filter(
|
||||
webhook=OuterRef('pk'),
|
||||
action_type=notification_type.action_type
|
||||
)
|
||||
webhooks = WebHook.objects.annotate(has_el=Exists(event_listener)).filter(
|
||||
organizer=logentry.organizer,
|
||||
has_el=True,
|
||||
enabled=True
|
||||
)
|
||||
if logentry.event_id:
|
||||
webhooks = webhooks.filter(
|
||||
Q(all_events=True) | Q(limit_events__pk=logentry.event_id)
|
||||
)
|
||||
# All webhooks that registered for this notification
|
||||
event_listener = WebHookEventListener.objects.filter(
|
||||
webhook=OuterRef('pk'),
|
||||
action_type=notification_type.action_type
|
||||
)
|
||||
|
||||
for wh in webhooks:
|
||||
send_webhook.apply_async(args=(logentry.id, notification_type.action_type, wh.pk))
|
||||
webhooks = WebHook.objects.annotate(has_el=Exists(event_listener)).filter(
|
||||
organizer=logentry.organizer,
|
||||
has_el=True,
|
||||
enabled=True
|
||||
)
|
||||
if logentry.event_id:
|
||||
webhooks = webhooks.filter(
|
||||
Q(all_events=True) | Q(limit_events__pk=logentry.event_id)
|
||||
)
|
||||
|
||||
for wh in webhooks:
|
||||
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)
|
||||
@@ -336,7 +250,7 @@ def send_webhook(self, logentry_id: int, action_type: str, webhook_id: int):
|
||||
webhook.enabled = False
|
||||
webhook.save()
|
||||
elif resp.status_code > 299:
|
||||
raise self.retry(countdown=2 ** (self.request.retries * 2)) # max is 2 ** (8*2) = 65536 seconds = ~18 hours
|
||||
raise self.retry(countdown=2 ** (self.request.retries * 2))
|
||||
except RequestException as e:
|
||||
WebHookCall.objects.create(
|
||||
webhook=webhook,
|
||||
@@ -348,6 +262,6 @@ def send_webhook(self, logentry_id: int, action_type: str, webhook_id: int):
|
||||
payload=json.dumps(payload),
|
||||
response_body=str(e)[:1024 * 1024]
|
||||
)
|
||||
raise self.retry(countdown=2 ** (self.request.retries * 2)) # max is 2 ** (8*2) = 65536 seconds = ~18 hours
|
||||
raise self.retry(countdown=2 ** (self.request.retries * 2))
|
||||
except MaxRetriesExceededError:
|
||||
pass
|
||||
|
||||
@@ -73,8 +73,8 @@ banlist = [
|
||||
"wtf"
|
||||
]
|
||||
|
||||
banlist_regex = re.compile('(' + '|'.join(banlist) + ')')
|
||||
blacklist_regex = re.compile('(' + '|'.join(banlist) + ')')
|
||||
|
||||
|
||||
def banned(string):
|
||||
return bool(banlist_regex.search(string.lower()))
|
||||
return bool(blacklist_regex.search(string.lower()))
|
||||
|
||||
@@ -41,7 +41,7 @@ class MailExporter(BaseExporter):
|
||||
initial=[Order.STATUS_PENDING, Order.STATUS_PAID],
|
||||
choices=Order.STATUS_CHOICE,
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
required=True
|
||||
required=False
|
||||
)),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -53,23 +53,9 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
initial=True,
|
||||
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):
|
||||
tax_rates = set(
|
||||
a for a
|
||||
@@ -164,10 +150,6 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
headers.append(_('Comment'))
|
||||
headers.append(_('Positions'))
|
||||
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
|
||||
|
||||
@@ -181,23 +163,6 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
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 = {
|
||||
(o['order__id'], o['tax_rate']): o for o in
|
||||
OrderPosition.objects.values('tax_rate', 'order__id').order_by().annotate(
|
||||
@@ -269,14 +234,6 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
str(self.providers.get(p, p)) for p in sorted(set((order.payment_providers or '').split(',')))
|
||||
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
|
||||
|
||||
def iterate_fees(self, form_data: dict):
|
||||
@@ -436,7 +393,6 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
_('Seat zone'),
|
||||
_('Seat row'),
|
||||
_('Seat number'),
|
||||
_('Order comment'),
|
||||
]
|
||||
|
||||
questions = list(Question.objects.filter(event__in=self.events))
|
||||
@@ -532,7 +488,6 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
else:
|
||||
row += ['', '', '', '', '']
|
||||
|
||||
row.append(order.comment)
|
||||
acache = {}
|
||||
for a in op.answers.all():
|
||||
# We do not want to localize Date, Time and Datetime question answers, as those can lead
|
||||
@@ -573,7 +528,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
row += [''] * (8 + (len(name_scheme['fields']) if name_scheme and len(name_scheme['fields']) > 1 else 0))
|
||||
row += [
|
||||
order.sales_channel,
|
||||
order.locale,
|
||||
order.locale
|
||||
]
|
||||
row.append(', '.join([
|
||||
str(self.providers.get(p, p)) for p in sorted(set((op.payment_providers or '').split(',')))
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import hashlib
|
||||
import ipaddress
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.password_validation import (
|
||||
password_validators_help_texts, validate_password,
|
||||
)
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.models import User
|
||||
from pretix.helpers.dicts import move_to_end
|
||||
from pretix.helpers.http import get_client_ip
|
||||
|
||||
|
||||
class LoginForm(forms.Form):
|
||||
@@ -23,7 +18,6 @@ class LoginForm(forms.Form):
|
||||
|
||||
error_messages = {
|
||||
'invalid_login': _("This combination of credentials is not known to our system."),
|
||||
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),
|
||||
'inactive': _("This account is inactive.")
|
||||
}
|
||||
|
||||
@@ -45,36 +39,10 @@ class LoginForm(forms.Form):
|
||||
else:
|
||||
move_to_end(self.fields, 'keep_logged_in')
|
||||
|
||||
@cached_property
|
||||
def ratelimit_key(self):
|
||||
if not settings.HAS_REDIS:
|
||||
return None
|
||||
client_ip = get_client_ip(self.request)
|
||||
if not client_ip:
|
||||
return None
|
||||
try:
|
||||
client_ip = ipaddress.ip_address(client_ip)
|
||||
except ValueError:
|
||||
# Web server not set up correctly
|
||||
return None
|
||||
if client_ip.is_private:
|
||||
# This is the private IP of the server, web server not set up correctly
|
||||
return None
|
||||
return 'pretix_login_{}'.format(hashlib.sha1(str(client_ip).encode()).hexdigest())
|
||||
|
||||
def clean(self):
|
||||
if all(k in self.cleaned_data for k, f in self.fields.items() if f.required):
|
||||
if self.ratelimit_key:
|
||||
from django_redis import get_redis_connection
|
||||
rc = get_redis_connection("redis")
|
||||
cnt = rc.get(self.ratelimit_key)
|
||||
if cnt and int(cnt) > 10:
|
||||
raise forms.ValidationError(self.error_messages['rate_limit'], code='rate_limit')
|
||||
self.user_cache = self.backend.form_authenticate(self.request, self.cleaned_data)
|
||||
if self.user_cache is None:
|
||||
if self.ratelimit_key:
|
||||
rc.incr(self.ratelimit_key)
|
||||
rc.expire(self.ratelimit_key, 300)
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['invalid_login'],
|
||||
code='invalid_login'
|
||||
|
||||
@@ -34,9 +34,7 @@ from pretix.base.forms.widgets import (
|
||||
)
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import InvoiceAddress, Question, QuestionOption
|
||||
from pretix.base.models.tax import (
|
||||
EU_COUNTRIES, cc_to_vat_prefix, is_eu_country,
|
||||
)
|
||||
from pretix.base.models.tax import EU_COUNTRIES, cc_to_vat_prefix
|
||||
from pretix.base.settings import (
|
||||
COUNTRIES_WITH_STATE_IN_ADDRESS, PERSON_NAME_SALUTATIONS,
|
||||
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS,
|
||||
@@ -650,7 +648,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
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.
|
||||
if cc and not is_eu_country(cc) and fprefix + 'vat_id' in self.data:
|
||||
if cc and cc not in EU_COUNTRIES and fprefix + 'vat_id' in self.data:
|
||||
self.data = self.data.copy()
|
||||
del self.data[fprefix + 'vat_id']
|
||||
|
||||
@@ -700,7 +698,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
if not data.get('is_business'):
|
||||
data['company'] = ''
|
||||
data['vat_id'] = ''
|
||||
if data.get('is_business') and not is_eu_country(data.get('country')):
|
||||
if data.get('is_business') and not data.get('country') in EU_COUNTRIES:
|
||||
data['vat_id'] = ''
|
||||
if self.event.settings.invoice_address_required:
|
||||
if data.get('is_business') and not data.get('company'):
|
||||
@@ -724,7 +722,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
self.cleaned_data['country'] = ''
|
||||
if self.validate_vat_id and self.instance.vat_id_validated and 'vat_id' not in self.changed_data:
|
||||
pass
|
||||
elif self.validate_vat_id and data.get('is_business') and is_eu_country(data.get('country')) and data.get('vat_id'):
|
||||
elif self.validate_vat_id and data.get('is_business') and data.get('country') in EU_COUNTRIES and data.get('vat_id'):
|
||||
if data.get('vat_id')[:2] != cc_to_vat_prefix(str(data.get('country'))):
|
||||
raise ValidationError(_('Your VAT ID does not match the selected country.'))
|
||||
try:
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.hashers import check_password
|
||||
from django.contrib.auth.password_validation import (
|
||||
password_validators_help_texts, validate_password,
|
||||
@@ -20,7 +19,6 @@ class UserSettingsForm(forms.ModelForm):
|
||||
"address or password."),
|
||||
'pw_current_wrong': _("The current password you entered was not correct."),
|
||||
'pw_mismatch': _("Please enter the same password twice"),
|
||||
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),
|
||||
}
|
||||
|
||||
old_pw = forms.CharField(max_length=255,
|
||||
@@ -66,18 +64,6 @@ class UserSettingsForm(forms.ModelForm):
|
||||
|
||||
def clean_old_pw(self):
|
||||
old_pw = self.cleaned_data.get('old_pw')
|
||||
|
||||
if old_pw and settings.HAS_REDIS:
|
||||
from django_redis import get_redis_connection
|
||||
rc = get_redis_connection("redis")
|
||||
cnt = rc.incr('pretix_pwchange_%s' % self.user.pk)
|
||||
rc.expire('pretix_pwchange_%s' % self.user.pk, 300)
|
||||
if cnt > 10:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['rate_limit'],
|
||||
code='rate_limit',
|
||||
)
|
||||
|
||||
if old_pw and not check_password(old_pw, self.user.password):
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['pw_current_wrong'],
|
||||
|
||||
@@ -13,10 +13,6 @@ from ...signals import periodic_task
|
||||
class Command(BaseCommand):
|
||||
help = "Run periodic tasks"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--tasks', action='store', type=str, help='Only execute the tasks with this name '
|
||||
'(dotted path, comma separation)')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
verbosity = int(options['verbosity'])
|
||||
|
||||
@@ -24,13 +20,8 @@ class Command(BaseCommand):
|
||||
return
|
||||
|
||||
for receiver in periodic_task._live_receivers(self):
|
||||
name = f'{receiver.__module__}.{receiver.__name__}'
|
||||
if options.get('tasks'):
|
||||
if name not in options.get('tasks').split(','):
|
||||
continue
|
||||
|
||||
if verbosity > 1:
|
||||
self.stdout.write(f'Running {name}…')
|
||||
self.stdout.write(f'Running {receiver.__module__}.{receiver.__name__}…')
|
||||
t0 = time.time()
|
||||
try:
|
||||
r = receiver(signal=periodic_task, sender=self)
|
||||
@@ -47,6 +38,6 @@ class Command(BaseCommand):
|
||||
else:
|
||||
if options.get('verbosity') > 1:
|
||||
if r is SKIPPED:
|
||||
self.stdout.write(self.style.SUCCESS(f'Skipped {name}'))
|
||||
self.stdout.write(self.style.SUCCESS(f'Skipped {receiver.__module__}.{receiver.__name__}'))
|
||||
else:
|
||||
self.stdout.write(self.style.SUCCESS(f'Completed {name} in {round(time.time() - t0, 3)}s'))
|
||||
self.stdout.write(self.style.SUCCESS(f'Completed {receiver.__module__}.{receiver.__name__} in {round(time.time() - t0, 3)}s'))
|
||||
|
||||
@@ -3,8 +3,7 @@ from urllib.parse import urlsplit
|
||||
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
from django.http import Http404, HttpRequest, HttpResponse
|
||||
from django.middleware.common import CommonMiddleware
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.urls import get_script_prefix
|
||||
from django.utils import timezone, translation
|
||||
from django.utils.cache import patch_vary_headers
|
||||
@@ -253,15 +252,3 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
del resp['Content-Security-Policy']
|
||||
|
||||
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
|
||||
|
||||
@@ -482,7 +482,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='secret',
|
||||
field=models.CharField(db_index=True, default="invalid", max_length=64),
|
||||
field=models.CharField(db_index=True, default=pretix.base.models.orders.generate_position_secret, max_length=64),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
|
||||
@@ -17,6 +17,6 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='secret',
|
||||
field=models.CharField(default="invalid", max_length=64),
|
||||
field=models.CharField(default=pretix.base.models.orders.generate_position_secret, max_length=64),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -38,7 +38,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='orderposition',
|
||||
name='secret',
|
||||
field=models.CharField(db_index=True, default="invalid", max_length=64),
|
||||
field=models.CharField(db_index=True, default=pretix.base.models.orders.generate_position_secret, max_length=64),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voucher',
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# Generated by Django 3.0.11 on 2020-12-18 18:10
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0162_remove_seat_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='cachedfile',
|
||||
name='session_key',
|
||||
field=models.TextField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cachedfile',
|
||||
name='web_download',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 3.0.9 on 2020-10-15 16:12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0163_device_security_profile'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='subevent',
|
||||
name='last_modified',
|
||||
field=models.DateTimeField(auto_now=True, db_index=True),
|
||||
),
|
||||
]
|
||||
@@ -1,29 +0,0 @@
|
||||
# Generated by Django 3.0.10 on 2020-10-15 19:24
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0164_subevent_last_modified'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='orderposition',
|
||||
name='secret',
|
||||
field=models.CharField(db_index=True, max_length=64),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RevokedTicketSecret',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('secret', models.TextField(db_index=True)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='revoked_secrets', to='pretixbase.Event')),
|
||||
('position', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='revoked_secrets', to='pretixbase.OrderPosition')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 3.0.10 on 2020-10-15 20:29
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0165_auto_20201015_1924'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='orderposition',
|
||||
name='secret',
|
||||
field=models.CharField(db_index=True, max_length=255),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 3.0.10 on 2020-10-20 06:24
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0166_auto_20201015_2029'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='checkinlist',
|
||||
name='exit_all_at',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,33 +0,0 @@
|
||||
# Generated by Django 3.0.9 on 2020-10-23 14:47
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0167_checkinlist_exit_all_at'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Gate',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=190)),
|
||||
('identifier', models.CharField(max_length=190)),
|
||||
('organizer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='gates', to='pretixbase.Organizer')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='checkin',
|
||||
name='gate',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='checkins', to='pretixbase.Gate'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='gate',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='devices', to='pretixbase.Gate'),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 3.0.10 on 2020-10-24 15:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0168_auto_20201023_1447'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='checkinlist',
|
||||
name='gates',
|
||||
field=models.ManyToManyField(to='pretixbase.Gate'),
|
||||
),
|
||||
]
|
||||
@@ -1,20 +0,0 @@
|
||||
# Generated by Django 3.0.9 on 2020-11-23 15:51
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def remove_old_settings(app, schema_editor):
|
||||
EventSettingsStore = app.get_model('pretixbase', 'Event_SettingsStore')
|
||||
|
||||
EventSettingsStore.objects.filter(key__startswith='payment_', key__endswith='__hidden_url').delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0169_checkinlist_gates'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(remove_old_settings, migrations.RunPython.noop)
|
||||
]
|
||||
@@ -1,14 +0,0 @@
|
||||
# Generated by Django 3.0.11 on 2020-12-22 10:30
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0170_remove_hidden_urls'),
|
||||
('pretixbase', '0162b_auto_20201218_1810'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
||||
@@ -2,7 +2,7 @@ from ..settings import GlobalSettingsObject_SettingsStore
|
||||
from .auth import U2FDevice, User, WebAuthnDevice
|
||||
from .base import CachedFile, LoggedModel, cachedfile_name
|
||||
from .checkin import Checkin, CheckinList
|
||||
from .devices import Device, Gate
|
||||
from .devices import Device
|
||||
from .event import (
|
||||
Event, Event_SettingsStore, EventLock, EventMetaProperty, EventMetaValue,
|
||||
RequiredAction, SubEvent, SubEventMetaValue, generate_invite_token,
|
||||
@@ -19,8 +19,8 @@ from .notifications import NotificationSetting
|
||||
from .orders import (
|
||||
AbstractPosition, CachedCombinedTicket, CachedTicket, CartPosition,
|
||||
InvoiceAddress, Order, OrderFee, OrderPayment, OrderPosition, OrderRefund,
|
||||
QuestionAnswer, RevokedTicketSecret, cachedcombinedticket_name,
|
||||
cachedticket_name, generate_position_secret, generate_secret,
|
||||
QuestionAnswer, cachedcombinedticket_name, cachedticket_name,
|
||||
generate_position_secret, generate_secret,
|
||||
)
|
||||
from .organizer import (
|
||||
Organizer, Organizer_SettingsStore, Team, TeamAPIToken, TeamInvite,
|
||||
|
||||
@@ -28,8 +28,6 @@ class CachedFile(models.Model):
|
||||
filename = models.CharField(max_length=255)
|
||||
type = models.CharField(max_length=255)
|
||||
file = models.FileField(null=True, blank=True, upload_to=cachedfile_name, max_length=255)
|
||||
web_download = models.BooleanField(default=True) # allow web download, True for backwards compatibility in plugins
|
||||
session_key = models.TextField(null=True, blank=True) # only allow download in this session
|
||||
|
||||
|
||||
@receiver(post_delete, sender=CachedFile)
|
||||
@@ -51,8 +49,9 @@ class LoggingMixin:
|
||||
:param user: The user performing the action (optional)
|
||||
"""
|
||||
from pretix.api.models import OAuthAccessToken, OAuthApplication
|
||||
from pretix.api.webhooks import notify_webhooks
|
||||
from pretix.api.webhooks import get_all_webhook_events, notify_webhooks
|
||||
|
||||
from ..notifications import get_all_notification_types
|
||||
from ..services.notifications import notify
|
||||
from .devices import Device
|
||||
from .event import Event
|
||||
@@ -94,11 +93,21 @@ class LoggingMixin:
|
||||
if save:
|
||||
logentry.save()
|
||||
|
||||
if logentry.notification_type:
|
||||
notify.apply_async(args=(logentry.pk,))
|
||||
if logentry.webhook_type:
|
||||
notify_webhooks.apply_async(args=(logentry.pk,))
|
||||
no_types = get_all_notification_types()
|
||||
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,))
|
||||
if wh_type:
|
||||
notify_webhooks.apply_async(args=(logentry.pk,))
|
||||
return logentry
|
||||
|
||||
|
||||
|
||||
@@ -21,11 +21,6 @@ class CheckinList(LoggedModel):
|
||||
default=False,
|
||||
help_text=_('With this option, people will be able to check in even if the '
|
||||
'order have not been paid.'))
|
||||
gates = models.ManyToManyField(
|
||||
'Gate', verbose_name=_("Gates"), blank=True,
|
||||
help_text=_("Does not have any effect for the validation of tickets, only for the automatic configuration of "
|
||||
"check-in devices.")
|
||||
)
|
||||
allow_entry_after_exit = models.BooleanField(
|
||||
verbose_name=_('Allow re-entering after an exit scan'),
|
||||
default=True
|
||||
@@ -35,10 +30,7 @@ class CheckinList(LoggedModel):
|
||||
help_text=_('Use this option to turn off warnings if a ticket is scanned a second time.'),
|
||||
default=False
|
||||
)
|
||||
exit_all_at = models.DateTimeField(
|
||||
verbose_name=_('Automatically check out everyone at'),
|
||||
null=True, blank=True
|
||||
)
|
||||
|
||||
auto_checkin_sales_channels = MultiStringField(
|
||||
default=[],
|
||||
blank=True,
|
||||
@@ -70,7 +62,7 @@ class CheckinList(LoggedModel):
|
||||
return qs
|
||||
|
||||
@property
|
||||
def positions_inside(self):
|
||||
def inside_count(self):
|
||||
return self.positions.annotate(
|
||||
last_entry=Subquery(
|
||||
Checkin.objects.filter(
|
||||
@@ -95,11 +87,7 @@ class CheckinList(LoggedModel):
|
||||
& Q(
|
||||
Q(last_exit__isnull=True) | Q(last_exit__lt=F('last_entry'))
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def inside_count(self):
|
||||
return self.positions_inside.count()
|
||||
).count()
|
||||
|
||||
@property
|
||||
@scopes_disabled()
|
||||
@@ -164,9 +152,6 @@ class Checkin(models.Model):
|
||||
device = models.ForeignKey(
|
||||
'pretixbase.Device', related_name='checkins', on_delete=models.PROTECT, null=True, blank=True
|
||||
)
|
||||
gate = models.ForeignKey(
|
||||
'pretixbase.Gate', related_name='checkins', on_delete=models.SET_NULL, null=True, blank=True
|
||||
)
|
||||
auto_checked_in = models.BooleanField(default=False)
|
||||
|
||||
objects = ScopedManager(organizer='position__order__event__organizer')
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import string
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import Max
|
||||
from django.utils.crypto import get_random_string
|
||||
@@ -35,64 +34,12 @@ def generate_api_token():
|
||||
return token
|
||||
|
||||
|
||||
class Gate(LoggedModel):
|
||||
organizer = models.ForeignKey(
|
||||
'pretixbase.Organizer',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='gates'
|
||||
)
|
||||
name = models.CharField(
|
||||
verbose_name=_("Name"),
|
||||
max_length=190,
|
||||
)
|
||||
identifier = models.CharField(
|
||||
max_length=190, blank=True,
|
||||
verbose_name=_("Internal identifier"),
|
||||
help_text=_('You can enter any value here to make it easier to match the data with other sources. If you do '
|
||||
'not input one, we will generate one automatically.')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ('name',)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def clean_identifier(self, code):
|
||||
Gate._clean_identifier(self.organizer, code, self)
|
||||
|
||||
@staticmethod
|
||||
def _clean_identifier(organizer, code, instance=None):
|
||||
qs = Gate.objects.filter(organizer=organizer, identifier__iexact=code)
|
||||
if instance:
|
||||
qs = qs.exclude(pk=instance.pk)
|
||||
if qs.exists():
|
||||
raise ValidationError(_('This identifier is already used for a different question.'))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.identifier:
|
||||
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
|
||||
while True:
|
||||
code = get_random_string(length=8, allowed_chars=charset)
|
||||
if not Gate.objects.filter(organizer=self.organizer, identifier=code).exists():
|
||||
self.identifier = code
|
||||
break
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class Device(LoggedModel):
|
||||
organizer = models.ForeignKey(
|
||||
'pretixbase.Organizer',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='devices'
|
||||
)
|
||||
gate = models.ForeignKey(
|
||||
'pretixbase.Gate',
|
||||
verbose_name=_('Gate'),
|
||||
on_delete=models.SET_NULL,
|
||||
null=True, blank=True,
|
||||
related_name='devices'
|
||||
)
|
||||
device_id = models.PositiveIntegerField()
|
||||
unique_serial = models.CharField(max_length=190, default=generate_serial, unique=True)
|
||||
initialization_token = models.CharField(max_length=190, default=generate_initialization_token, unique=True)
|
||||
@@ -222,15 +169,3 @@ class Device(LoggedModel):
|
||||
return self.organizer.events.all()
|
||||
else:
|
||||
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,49 +118,25 @@ class EventMixin:
|
||||
def timezone(self):
|
||||
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
|
||||
def presale_has_ended(self):
|
||||
"""
|
||||
Is true, when ``presale_end`` is set and in the past.
|
||||
"""
|
||||
if self.effective_presale_end:
|
||||
return now() > self.effective_presale_end
|
||||
if self.presale_end:
|
||||
return now() > self.presale_end
|
||||
elif self.date_to:
|
||||
return now() > self.date_to
|
||||
else:
|
||||
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
|
||||
def presale_is_running(self):
|
||||
"""
|
||||
Is true, when ``presale_end`` is not set or in the future and ``presale_start`` is not
|
||||
set or in the past.
|
||||
"""
|
||||
if self.effective_presale_start and now() < self.effective_presale_start:
|
||||
if self.presale_start and now() < self.presale_start:
|
||||
return False
|
||||
return not self.presale_has_ended
|
||||
|
||||
@@ -268,34 +244,6 @@ class EventMixin:
|
||||
return Quota.AVAILABILITY_RESERVED
|
||||
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')
|
||||
class Event(EventMixin, LoggedModel):
|
||||
@@ -446,7 +394,7 @@ class Event(EventMixin, LoggedModel):
|
||||
if img:
|
||||
return urljoin(build_absolute_uri(self, 'presale:event.index'), img)
|
||||
|
||||
def _seats(self, ignore_voucher=None):
|
||||
def free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False):
|
||||
from .seating import Seat
|
||||
|
||||
qs_annotated = Seat.annotated(self.seats, self.pk, None,
|
||||
@@ -454,7 +402,13 @@ class Event(EventMixin, LoggedModel):
|
||||
minimal_distance=self.settings.seating_minimal_distance,
|
||||
distance_only_within_row=self.settings.seating_distance_within_row)
|
||||
|
||||
return qs_annotated
|
||||
qs = qs_annotated.filter(has_order=False, has_cart=False, has_voucher=False)
|
||||
if self.settings.seating_minimal_distance > 0:
|
||||
qs = qs.filter(has_closeby_taken=False)
|
||||
|
||||
if not (sales_channel in self.settings.seating_allow_blocked_seats_for_channel or include_blocked):
|
||||
qs = qs.filter(blocked=False)
|
||||
return qs
|
||||
|
||||
@property
|
||||
def presale_has_ended(self):
|
||||
@@ -553,14 +507,11 @@ class Event(EventMixin, LoggedModel):
|
||||
def copy_data_from(self, other):
|
||||
from ..signals import event_copy_data
|
||||
from . import (
|
||||
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, Question,
|
||||
Quota,
|
||||
Item, ItemAddOn, ItemCategory, ItemMetaValue, Question, Quota,
|
||||
)
|
||||
|
||||
self.plugins = other.plugins
|
||||
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.save()
|
||||
self.log_action('pretix.object.cloned', data={'source': other.slug, 'source_id': other.pk})
|
||||
@@ -622,14 +573,6 @@ class Event(EventMixin, LoggedModel):
|
||||
ia.addon_category = category_map[ia.addon_category.pk]
|
||||
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'):
|
||||
items = list(q.items.all())
|
||||
vars = list(q.variations.all())
|
||||
@@ -719,14 +662,7 @@ class Event(EventMixin, LoggedModel):
|
||||
s.product = item_map[s.product_id]
|
||||
s.save()
|
||||
|
||||
skip_settings = (
|
||||
'ticket_secrets_pretix_sig1_pubkey',
|
||||
'ticket_secrets_pretix_sig1_privkey',
|
||||
)
|
||||
for s in other.settings._objects.all():
|
||||
if s.key in skip_settings:
|
||||
continue
|
||||
|
||||
s.object = self
|
||||
s.pk = None
|
||||
if s.value.startswith('file://'):
|
||||
@@ -818,31 +754,6 @@ class Event(EventMixin, LoggedModel):
|
||||
renderers[pp.identifier] = pp
|
||||
return renderers
|
||||
|
||||
@cached_property
|
||||
def ticket_secret_generators(self) -> dict:
|
||||
"""
|
||||
Returns a dictionary of cached initialized ticket secret generators mapped by their identifiers.
|
||||
"""
|
||||
from ..signals import register_ticket_secret_generators
|
||||
|
||||
responses = register_ticket_secret_generators.send(self)
|
||||
renderers = {}
|
||||
for receiver, response in responses:
|
||||
if not isinstance(response, list):
|
||||
response = [response]
|
||||
for p in response:
|
||||
pp = p(self)
|
||||
renderers[pp.identifier] = pp
|
||||
return renderers
|
||||
|
||||
@property
|
||||
def ticket_secret_generator(self):
|
||||
"""
|
||||
Returns the currently configured ticket secret generator.
|
||||
"""
|
||||
tsgs = self.ticket_secret_generators
|
||||
return tsgs[self.settings.ticket_secret_generator]
|
||||
|
||||
def get_data_shredders(self) -> dict:
|
||||
"""
|
||||
Returns a dictionary of initialized data shredders mapped by their identifiers.
|
||||
@@ -1125,9 +1036,6 @@ class SubEvent(EventMixin, LoggedModel):
|
||||
)
|
||||
seating_plan = models.ForeignKey('SeatingPlan', on_delete=models.PROTECT, null=True, blank=True,
|
||||
related_name='subevents')
|
||||
last_modified = models.DateTimeField(
|
||||
auto_now=True, db_index=True
|
||||
)
|
||||
|
||||
items = models.ManyToManyField('Item', through='SubEventItem')
|
||||
variations = models.ManyToManyField('ItemVariation', through='SubEventItemVariation')
|
||||
@@ -1146,13 +1054,19 @@ class SubEvent(EventMixin, LoggedModel):
|
||||
date_format(self.date_from.astimezone(self.timezone), "TIME_FORMAT") if self.settings.show_times else ""
|
||||
).strip()
|
||||
|
||||
def _seats(self, ignore_voucher=None):
|
||||
def free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False):
|
||||
from .seating import Seat
|
||||
qs_annotated = Seat.annotated(self.seats, self.event_id, self,
|
||||
ignore_voucher_id=ignore_voucher.pk if ignore_voucher else None,
|
||||
minimal_distance=self.settings.seating_minimal_distance,
|
||||
distance_only_within_row=self.settings.seating_distance_within_row)
|
||||
return qs_annotated
|
||||
qs = qs_annotated.filter(has_order=False, has_cart=False, has_voucher=False)
|
||||
if self.settings.seating_minimal_distance > 0:
|
||||
qs = qs.filter(has_closeby_taken=False)
|
||||
|
||||
if not (sales_channel in self.settings.seating_allow_blocked_seats_for_channel or include_blocked):
|
||||
qs = qs.filter(blocked=False)
|
||||
return qs
|
||||
|
||||
@cached_property
|
||||
def settings(self):
|
||||
|
||||
@@ -314,7 +314,7 @@ class Item(LoggedModel):
|
||||
)
|
||||
allow_waitinglist = models.BooleanField(
|
||||
verbose_name=_("Show a waiting list for this ticket"),
|
||||
help_text=_("This will only work if waiting lists are enabled for this event."),
|
||||
help_text=_("This will only work of waiting lists are enabled for this event."),
|
||||
default=True
|
||||
)
|
||||
show_quota_left = models.NullBooleanField(
|
||||
|
||||
@@ -63,42 +63,14 @@ class LogEntry(models.Model):
|
||||
return response
|
||||
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
|
||||
def organizer(self):
|
||||
from .organizer import Organizer
|
||||
|
||||
if self.event:
|
||||
return self.event.organizer
|
||||
elif hasattr(self.content_object, 'event'):
|
||||
return self.content_object.event.organizer
|
||||
elif hasattr(self.content_object, 'organizer'):
|
||||
return self.content_object.organizer
|
||||
elif isinstance(self.content_object, Organizer):
|
||||
return self.content_object
|
||||
return None
|
||||
|
||||
@cached_property
|
||||
@@ -216,16 +188,3 @@ class LogEntry(models.Model):
|
||||
|
||||
def delete(self, using=None, keep_parents=False):
|
||||
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,))
|
||||
|
||||
@@ -57,7 +57,8 @@ def generate_secret():
|
||||
|
||||
|
||||
def generate_position_secret():
|
||||
raise TypeError("Function no longer exists, use secret generators")
|
||||
# Exclude o,0,1,i,l to avoid confusion with bad fonts/printers
|
||||
return get_random_string(length=settings.ENTROPY['ticket_secret'], allowed_chars='abcdefghjkmnpqrstuvwxyz23456789')
|
||||
|
||||
|
||||
class Order(LockModel, LoggedModel):
|
||||
@@ -1600,10 +1601,6 @@ class OrderPayment(models.Model):
|
||||
'local_id': r.local_id,
|
||||
'provider': r.provider,
|
||||
})
|
||||
|
||||
if self.order.pending_sum + r.amount == Decimal('0.00'):
|
||||
self.refund.done()
|
||||
|
||||
return r
|
||||
|
||||
|
||||
@@ -1941,7 +1938,7 @@ class OrderPosition(AbstractPosition):
|
||||
max_digits=10, decimal_places=2,
|
||||
verbose_name=_('Tax value')
|
||||
)
|
||||
secret = models.CharField(max_length=255, null=False, blank=False, db_index=True)
|
||||
secret = models.CharField(max_length=64, default=generate_position_secret, db_index=True)
|
||||
web_secret = models.CharField(max_length=32, default=generate_secret, db_index=True)
|
||||
pseudonymization_id = models.CharField(
|
||||
max_length=16,
|
||||
@@ -2034,18 +2031,13 @@ class OrderPosition(AbstractPosition):
|
||||
self.tax_rate = Decimal('0.00')
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
from pretix.base.secrets import assign_ticket_secret
|
||||
|
||||
if self.tax_rate is None:
|
||||
self._calculate_tax()
|
||||
self.order.touch()
|
||||
if not self.pk:
|
||||
while not self.secret or OrderPosition.all.filter(
|
||||
secret=self.secret, order__event__organizer_id=self.order.event.organizer_id
|
||||
).exists():
|
||||
assign_ticket_secret(
|
||||
event=self.order.event, position=self, force_invalidate=True, save=False
|
||||
)
|
||||
while OrderPosition.all.filter(secret=self.secret,
|
||||
order__event__organizer_id=self.order.event.organizer_id).exists():
|
||||
self.secret = generate_position_secret()
|
||||
|
||||
if not self.pseudonymization_id:
|
||||
self.assign_pseudonymization_id()
|
||||
@@ -2075,7 +2067,7 @@ class OrderPosition(AbstractPosition):
|
||||
user: User=None, headers: dict=None, sender: str=None, invoices: list=None,
|
||||
auth=None, attach_tickets=False):
|
||||
"""
|
||||
Sends an email to the attendee. Basically, this method does two things:
|
||||
Sends an email to the user that placed this order. Basically, this method does two things:
|
||||
|
||||
* Call ``pretix.base.services.mail.mail`` with useful values for the ``event``, ``locale``, ``recipient`` and
|
||||
``order`` parameters.
|
||||
@@ -2334,18 +2326,6 @@ class CancellationRequest(models.Model):
|
||||
refund_as_giftcard = models.BooleanField(default=False)
|
||||
|
||||
|
||||
class RevokedTicketSecret(models.Model):
|
||||
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name='revoked_secrets')
|
||||
position = models.ForeignKey(
|
||||
OrderPosition,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='revoked_secrets',
|
||||
null=True,
|
||||
)
|
||||
secret = models.TextField(db_index=True)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
@receiver(post_delete, sender=CachedTicket)
|
||||
def cachedticket_delete(sender, instance, **kwargs):
|
||||
if instance.file:
|
||||
|
||||
@@ -357,15 +357,3 @@ class TeamAPIToken(models.Model):
|
||||
return self.team.organizer.events.all()
|
||||
else:
|
||||
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_label = models.CharField(max_length=190, null=True)
|
||||
seat_guid = models.CharField(max_length=190, db_index=True)
|
||||
product = models.ForeignKey('Item', null=True, blank=True, related_name='seats', on_delete=models.SET_NULL)
|
||||
product = models.ForeignKey('Item', null=True, blank=True, related_name='seats', on_delete=models.CASCADE)
|
||||
blocked = models.BooleanField(default=False)
|
||||
sorting_rank = models.BigIntegerField(default=0)
|
||||
x = models.FloatField(null=True)
|
||||
|
||||
@@ -4,7 +4,6 @@ from decimal import Decimal
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.formats import localize
|
||||
from django.utils.timezone import get_current_timezone, now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from i18nfield.fields import I18nCharField
|
||||
|
||||
@@ -86,14 +85,6 @@ EU_CURRENCIES = {
|
||||
}
|
||||
|
||||
|
||||
def is_eu_country(cc):
|
||||
cc = str(cc)
|
||||
if cc == 'GB':
|
||||
return now().astimezone(get_current_timezone()).year <= 2020
|
||||
else:
|
||||
return cc in EU_COUNTRIES
|
||||
|
||||
|
||||
def cc_to_vat_prefix(country_code):
|
||||
if country_code == 'GR':
|
||||
return 'EL'
|
||||
@@ -136,9 +127,6 @@ class TaxRule(LoggedModel):
|
||||
class Meta:
|
||||
ordering = ('event', 'rate', 'id')
|
||||
|
||||
class SaleNotAllowed(Exception):
|
||||
pass
|
||||
|
||||
def allow_delete(self):
|
||||
from pretix.base.models.orders import OrderFee, OrderPosition
|
||||
|
||||
@@ -181,8 +169,6 @@ class TaxRule(LoggedModel):
|
||||
return Decimal('0.00')
|
||||
if self.has_custom_rules:
|
||||
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:
|
||||
return Decimal(rule.get('rate'))
|
||||
return Decimal(self.rate)
|
||||
@@ -255,7 +241,7 @@ class TaxRule(LoggedModel):
|
||||
rules = self._custom_rules
|
||||
if invoice_address:
|
||||
for r in rules:
|
||||
if r['country'] == 'EU' and not is_eu_country(invoice_address.country):
|
||||
if r['country'] == 'EU' and str(invoice_address.country) not in EU_COUNTRIES:
|
||||
continue
|
||||
if r['country'] not in ('ZZ', 'EU') and r['country'] != str(invoice_address.country):
|
||||
continue
|
||||
@@ -279,7 +265,7 @@ class TaxRule(LoggedModel):
|
||||
if not invoice_address or not invoice_address.country:
|
||||
return False
|
||||
|
||||
if not is_eu_country(invoice_address.country):
|
||||
if str(invoice_address.country) not in EU_COUNTRIES:
|
||||
return False
|
||||
|
||||
if invoice_address.country == self.home_country:
|
||||
@@ -293,8 +279,6 @@ class TaxRule(LoggedModel):
|
||||
def _tax_applicable(self, invoice_address):
|
||||
if self._custom_rules:
|
||||
rule = self.get_matching_rule(invoice_address)
|
||||
if rule.get('action', 'vat') == 'block':
|
||||
raise self.SaleNotAllowed()
|
||||
return rule.get('action', 'vat') == 'vat'
|
||||
|
||||
if not self.eu_reverse_charge:
|
||||
@@ -305,7 +289,7 @@ class TaxRule(LoggedModel):
|
||||
# No country specified? Always apply VAT!
|
||||
return True
|
||||
|
||||
if not is_eu_country(invoice_address.country):
|
||||
if str(invoice_address.country) not in EU_COUNTRIES:
|
||||
# Non-EU country? Never apply VAT!
|
||||
return False
|
||||
|
||||
|
||||
@@ -641,7 +641,7 @@ class QuestionColumn(ImportColumn):
|
||||
for v in opt.answer.data.values():
|
||||
self.option_resolve_cache[v.strip()].add(opt)
|
||||
else:
|
||||
self.option_resolve_cache[opt.answer.data.strip()].add(opt)
|
||||
self.option_resolve_cache[opt.answer.strip()].add(opt)
|
||||
|
||||
else:
|
||||
self.option_resolve_cache[opt.answer.strip()].add(opt)
|
||||
@@ -661,7 +661,7 @@ class QuestionColumn(ImportColumn):
|
||||
if value not in self.option_resolve_cache:
|
||||
raise ValidationError(_('Invalid option selected.'))
|
||||
if len(self.option_resolve_cache[value]) > 1:
|
||||
raise ValidationError(_('Ambiguous option selected.'))
|
||||
raise ValidationError(_('Ambigous option selected.'))
|
||||
return list(self.option_resolve_cache[value])[0]
|
||||
|
||||
elif self.q.type == Question.TYPE_CHOICE_MULTIPLE:
|
||||
@@ -669,7 +669,7 @@ class QuestionColumn(ImportColumn):
|
||||
if any(v.strip() not in self.option_resolve_cache for v in values):
|
||||
raise ValidationError(_('Invalid option selected.'))
|
||||
if any(len(self.option_resolve_cache[v.strip()]) > 1 for v in values):
|
||||
raise ValidationError(_('Ambiguous option selected.'))
|
||||
raise ValidationError(_('Ambigous option selected.'))
|
||||
return [list(self.option_resolve_cache[v.strip()])[0] for v in values]
|
||||
|
||||
else:
|
||||
|
||||
@@ -513,7 +513,7 @@ class BasePaymentProvider:
|
||||
|
||||
return timing and pricing
|
||||
|
||||
def payment_form_render(self, request: HttpRequest, total: Decimal, order: Order=None) -> str:
|
||||
def payment_form_render(self, request: HttpRequest, total: Decimal) -> str:
|
||||
"""
|
||||
When the user selects this provider as their preferred payment method,
|
||||
they will be shown the HTML you return from this method.
|
||||
@@ -522,15 +522,13 @@ class BasePaymentProvider:
|
||||
and render the returned form. If your payment method doesn't require
|
||||
the user to fill out form fields, you should just return a paragraph
|
||||
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)
|
||||
template = get_template('pretixpresale/event/checkout_payment_form_default.html')
|
||||
ctx = {'request': request, 'form': form}
|
||||
return template.render(ctx)
|
||||
|
||||
def checkout_confirm_render(self, request, order: Order=None) -> str:
|
||||
def checkout_confirm_render(self, request) -> str:
|
||||
"""
|
||||
If the user has successfully filled in their payment data, they will be redirected
|
||||
to a confirmation page which lists all details of their order for a final review.
|
||||
@@ -539,8 +537,6 @@ class BasePaymentProvider:
|
||||
|
||||
In most cases, this should include a short summary of the user's input and
|
||||
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
|
||||
|
||||
@@ -721,7 +717,7 @@ class BasePaymentProvider:
|
||||
|
||||
The default implementation returns an empty string.
|
||||
|
||||
:param refund: The refund object
|
||||
:param order: The order object
|
||||
"""
|
||||
return ''
|
||||
|
||||
|
||||
@@ -48,9 +48,7 @@ DEFAULT_VARIABLES = OrderedDict((
|
||||
("secret", {
|
||||
"label": _("Ticket code (barcode content)"),
|
||||
"editor_sample": "tdmruoekvkpbv1o2mv8xccvqcikvr58u",
|
||||
"evaluate": lambda orderposition, order, event: (
|
||||
orderposition.secret[:30] + "…" if len(orderposition.secret) > 32 else orderposition.secret
|
||||
)
|
||||
"evaluate": lambda orderposition, order, event: orderposition.secret
|
||||
}),
|
||||
("order", {
|
||||
"label": _("Order code"),
|
||||
@@ -429,13 +427,8 @@ class Renderer:
|
||||
elif content == 'pseudonymization_id':
|
||||
content = op.pseudonymization_id
|
||||
|
||||
level = 'H'
|
||||
if len(content) > 32:
|
||||
level = 'M'
|
||||
if len(content) > 128:
|
||||
level = 'L'
|
||||
reqs = float(o['size']) * mm
|
||||
qrw = QrCodeWidget(content, barLevel=level, barHeight=reqs, barWidth=reqs)
|
||||
qrw = QrCodeWidget(content, barLevel='H', barHeight=reqs, barWidth=reqs)
|
||||
d = Drawing(reqs, reqs)
|
||||
d.add(qrw)
|
||||
qr_x = float(o['left']) * mm
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option java_package = "eu.pretix.libpretixsync.crypto.sig1";
|
||||
option java_outer_classname = "TicketProtos";
|
||||
|
||||
message Ticket {
|
||||
string seed = 1;
|
||||
int64 item = 2;
|
||||
int64 variation = 3;
|
||||
int64 subevent = 4;
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: pretix_sig1.proto
|
||||
|
||||
from google.protobuf import (
|
||||
descriptor as _descriptor, message as _message, reflection as _reflection,
|
||||
symbol_database as _symbol_database,
|
||||
)
|
||||
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor.FileDescriptor(
|
||||
name='pretix_sig1.proto',
|
||||
package='',
|
||||
syntax='proto3',
|
||||
serialized_options=b'\n\026eu.pretix.secrets.sig1B\014TicketProtos',
|
||||
create_key=_descriptor._internal_create_key,
|
||||
serialized_pb=b'\n\x11pretix_sig1.proto\"I\n\x06Ticket\x12\x0c\n\x04seed\x18\x01 \x01(\t\x12\x0c\n\x04item\x18\x02 \x01(\x03\x12\x11\n\tvariation\x18\x03 \x01(\x03\x12\x10\n\x08subevent\x18\x04 \x01(\x03\x42&\n\x16\x65u.pretix.secrets.sig1B\x0cTicketProtosb\x06proto3'
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
_TICKET = _descriptor.Descriptor(
|
||||
name='Ticket',
|
||||
full_name='Ticket',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
containing_type=None,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='seed', full_name='Ticket.seed', index=0,
|
||||
number=1, type=9, cpp_type=9, label=1,
|
||||
has_default_value=False, default_value=b"".decode('utf-8'),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='item', full_name='Ticket.item', index=1,
|
||||
number=2, type=3, cpp_type=2, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='variation', full_name='Ticket.variation', index=2,
|
||||
number=3, type=3, cpp_type=2, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='subevent', full_name='Ticket.subevent', index=3,
|
||||
number=4, type=3, cpp_type=2, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[],
|
||||
enum_types=[
|
||||
],
|
||||
serialized_options=None,
|
||||
is_extendable=False,
|
||||
syntax='proto3',
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=21,
|
||||
serialized_end=94,
|
||||
)
|
||||
|
||||
DESCRIPTOR.message_types_by_name['Ticket'] = _TICKET
|
||||
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
|
||||
|
||||
Ticket = _reflection.GeneratedProtocolMessageType('Ticket', (_message.Message,), {
|
||||
'DESCRIPTOR' : _TICKET,
|
||||
'__module__' : 'pretix_sig1_pb2'
|
||||
# @@protoc_insertion_point(class_scope:Ticket)
|
||||
})
|
||||
_sym_db.RegisterMessage(Ticket)
|
||||
|
||||
|
||||
DESCRIPTOR._options = None
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
@@ -1,213 +0,0 @@
|
||||
import base64
|
||||
import inspect
|
||||
import struct
|
||||
|
||||
from cryptography.hazmat.backends.openssl.backend import Backend
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||
from cryptography.hazmat.primitives.serialization.base import (
|
||||
Encoding, NoEncryption, PrivateFormat, PublicFormat, load_pem_private_key,
|
||||
load_pem_public_key,
|
||||
)
|
||||
from django.conf import settings
|
||||
from django.dispatch import receiver
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.models import Item, ItemVariation, SubEvent
|
||||
from pretix.base.secretgenerators import pretix_sig1_pb2
|
||||
from pretix.base.signals import register_ticket_secret_generators
|
||||
|
||||
|
||||
class BaseTicketSecretGenerator:
|
||||
"""
|
||||
This is the base class to be used for all ticket secret generators.
|
||||
"""
|
||||
|
||||
@property
|
||||
def verbose_name(self) -> str:
|
||||
"""
|
||||
A human-readable name for this generator. This should be short but self-explanatory.
|
||||
"""
|
||||
raise NotImplementedError() # NOQA
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
"""
|
||||
A short and unique identifier for this renderer. This should only contain lowercase letters
|
||||
and in most cases will be the same as your package name.
|
||||
"""
|
||||
raise NotImplementedError() # NOQA
|
||||
|
||||
def __init__(self, event):
|
||||
self.event = event
|
||||
|
||||
@property
|
||||
def use_revocation_list(self):
|
||||
"""
|
||||
If this attribute is set to ``True``, the system will set all no-longer-used secrets on a revocation list.
|
||||
This is not required for pretix' default method of just using random identifiers as ticket secrets
|
||||
since all ticket scans will be compared to the database. However, if your secret generation method
|
||||
is designed to allow offline verification without a ticket database, all invalidated/replaced
|
||||
secrets as well as all secrets of canceled tickets will need to go to a revocation list.
|
||||
"""
|
||||
return False
|
||||
|
||||
def generate_secret(self, item: Item, variation: ItemVariation = None, subevent: SubEvent = None,
|
||||
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``,
|
||||
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 algorithm is expected to conform to the following rules:
|
||||
|
||||
If ``force_invalidate`` is set to ``True``, the method MUST return a different secret than ``current_secret``,
|
||||
such that ``current_secret`` can get revoked.
|
||||
|
||||
If ``force_invalidate`` is set to ``False`` and ``item``, ``variation`` and ``subevent`` have the same value
|
||||
as when ``current_secret`` was generated, then this method MUST return ``current_secret`` unchanged.
|
||||
|
||||
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,
|
||||
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()
|
||||
|
||||
|
||||
class RandomTicketSecretGenerator(BaseTicketSecretGenerator):
|
||||
verbose_name = _('Random (default, works with all pretix apps)')
|
||||
identifier = 'random'
|
||||
use_revocation_list = False
|
||||
|
||||
def generate_secret(self, item: Item, variation: ItemVariation = None, subevent: SubEvent = None,
|
||||
attendee_name: str = None, current_secret: str = None, force_invalidate=False):
|
||||
if current_secret and not force_invalidate:
|
||||
return current_secret
|
||||
return get_random_string(
|
||||
length=settings.ENTROPY['ticket_secret'],
|
||||
# Exclude o,0,1,i,l to avoid confusion with bad fonts/printers
|
||||
allowed_chars='abcdefghjkmnpqrstuvwxyz23456789'
|
||||
)
|
||||
|
||||
|
||||
class Sig1TicketSecretGenerator(BaseTicketSecretGenerator):
|
||||
"""
|
||||
Secret generator for signed QR codes.
|
||||
|
||||
QR-code format:
|
||||
|
||||
- 1 Byte with the version of the scheme, currently 0x01
|
||||
- 2 Bytes length of the payload (big-endian) => n
|
||||
- 2 Bytes length of the signature (big-endian) => m
|
||||
- n Bytes payload (with protobuf encoding)
|
||||
- m Bytes ECDSA signature of Sign(payload)
|
||||
|
||||
The resulting string is REVERSED, to avoid all secrets of same length beginning with the same 10
|
||||
characters, which would make it impossible to search for secrets manually.
|
||||
"""
|
||||
verbose_name = _('pretix signature scheme 1 (for very large events, does not work with pretixSCAN on iOS and '
|
||||
'changes semantics of offline scanning – please refer to documentation or support for details)')
|
||||
identifier = 'pretix_sig1'
|
||||
use_revocation_list = True
|
||||
|
||||
def _generate_keys(self):
|
||||
privkey = Ed25519PrivateKey.generate()
|
||||
pubkey = privkey.public_key()
|
||||
self.event.settings.ticket_secrets_pretix_sig1_privkey = base64.b64encode(privkey.private_bytes(
|
||||
Encoding.PEM, PrivateFormat.PKCS8, NoEncryption()
|
||||
)).decode()
|
||||
self.event.settings.ticket_secrets_pretix_sig1_pubkey = base64.b64encode(pubkey.public_bytes(
|
||||
Encoding.PEM, PublicFormat.SubjectPublicKeyInfo
|
||||
)).decode()
|
||||
|
||||
def _sign_payload(self, payload):
|
||||
if not self.event.settings.ticket_secrets_pretix_sig1_privkey:
|
||||
self._generate_keys()
|
||||
privkey = load_pem_private_key(
|
||||
base64.b64decode(self.event.settings.ticket_secrets_pretix_sig1_privkey), None, Backend()
|
||||
)
|
||||
signature = privkey.sign(payload)
|
||||
return (
|
||||
bytes([0x01])
|
||||
+ struct.pack(">H", len(payload))
|
||||
+ struct.pack(">H", len(signature))
|
||||
+ payload
|
||||
+ signature
|
||||
)
|
||||
|
||||
def _parse(self, secret):
|
||||
try:
|
||||
rawbytes = base64.b64decode(secret[::-1])
|
||||
if rawbytes[0] != 1:
|
||||
raise ValueError('Invalid version')
|
||||
|
||||
payload_len = struct.unpack(">H", rawbytes[1:3])[0]
|
||||
sig_len = struct.unpack(">H", rawbytes[3:5])[0]
|
||||
payload = rawbytes[5:5 + payload_len]
|
||||
signature = rawbytes[5 + payload_len:5 + payload_len + sig_len]
|
||||
pubkey = load_pem_public_key(
|
||||
base64.b64decode(self.event.settings.ticket_secrets_pretix_sig1_pubkey), Backend()
|
||||
)
|
||||
pubkey.verify(signature, payload)
|
||||
t = pretix_sig1_pb2.Ticket()
|
||||
t.ParseFromString(payload)
|
||||
return t
|
||||
except:
|
||||
return None
|
||||
|
||||
def generate_secret(self, item: Item, variation: ItemVariation = None, subevent: SubEvent = None,
|
||||
current_secret: str = None, force_invalidate=False):
|
||||
if current_secret and not force_invalidate:
|
||||
ticket = self._parse(current_secret)
|
||||
if ticket:
|
||||
unchanged = (
|
||||
ticket.item == item.pk and
|
||||
ticket.variation == (variation.pk if variation else 0) and
|
||||
ticket.subevent == (subevent.pk if subevent else 0)
|
||||
)
|
||||
if unchanged:
|
||||
return current_secret
|
||||
|
||||
t = pretix_sig1_pb2.Ticket()
|
||||
t.seed = get_random_string(9)
|
||||
t.item = item.pk
|
||||
t.variation = variation.pk if variation else 0
|
||||
t.subevent = subevent.pk if subevent else 0
|
||||
payload = t.SerializeToString()
|
||||
result = base64.b64encode(self._sign_payload(payload)).decode()[::-1]
|
||||
return result
|
||||
|
||||
|
||||
@receiver(register_ticket_secret_generators, dispatch_uid="ticket_generator_default")
|
||||
def recv_classic(sender, **kwargs):
|
||||
return [RandomTicketSecretGenerator, Sig1TicketSecretGenerator]
|
||||
|
||||
|
||||
def assign_ticket_secret(event, position, force_invalidate_if_revokation_list_used=False, force_invalidate=False, save=True):
|
||||
gen = event.ticket_secret_generator
|
||||
if gen.use_revocation_list and force_invalidate_if_revokation_list_used:
|
||||
force_invalidate = True
|
||||
|
||||
kwargs = {}
|
||||
if 'attendee_name' in inspect.signature(gen.generate_secret).parameters:
|
||||
kwargs['attendee_name'] = position.attendee_name
|
||||
secret = gen.generate_secret(
|
||||
item=position.item,
|
||||
variation=position.variation,
|
||||
subevent=position.subevent,
|
||||
current_secret=position.secret,
|
||||
force_invalidate=force_invalidate,
|
||||
**kwargs
|
||||
)
|
||||
changed = position.secret != secret
|
||||
if position.secret and changed and gen.use_revocation_list:
|
||||
position.revoked_secrets.create(event=event, secret=position.secret)
|
||||
position.secret = secret
|
||||
if save and changed:
|
||||
position.save()
|
||||
@@ -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:
|
||||
real_subject = str(subject).format_map(TolerantDict(email_context))
|
||||
email_context = get_email_context(event_or_subevent=p.subevent or order.event,
|
||||
email_context = get_email_context(event_or_subevent=subevent or order.event,
|
||||
event=order.event,
|
||||
refund_amount=refund_amount,
|
||||
position_or_address=p,
|
||||
@@ -82,12 +82,11 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s
|
||||
|
||||
|
||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
|
||||
def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
|
||||
keep_fee_fixed: str, keep_fee_per_ticket: str, keep_fee_percentage: str, keep_fees: list=None,
|
||||
manual_refund: bool=False, send: bool=False, send_subject: dict=None, send_message: dict=None,
|
||||
def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_fixed: str,
|
||||
keep_fee_percentage: str, keep_fees: list=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={},
|
||||
user: int=None, refund_as_giftcard: bool=False, giftcard_expires=None, giftcard_conditions=None,
|
||||
subevents_from: str=None, subevents_to: str=None):
|
||||
user: int=None, refund_as_giftcard: bool=False, giftcard_expires=None, giftcard_conditions=None):
|
||||
send_subject = LazyI18nString(send_subject)
|
||||
send_message = LazyI18nString(send_message)
|
||||
send_waitinglist_subject = LazyI18nString(send_waitinglist_subject)
|
||||
@@ -103,20 +102,14 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
|
||||
pcnt__gt=0
|
||||
).all()
|
||||
|
||||
if subevent or subevents_from:
|
||||
if 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))
|
||||
if subevent:
|
||||
subevent = event.subevents.get(pk=subevent)
|
||||
|
||||
has_subevent = OrderPosition.objects.filter(order_id=OuterRef('pk')).filter(
|
||||
subevent__in=subevents
|
||||
subevent=subevent
|
||||
)
|
||||
has_other_subevent = OrderPosition.objects.filter(order_id=OuterRef('pk')).exclude(
|
||||
subevent__in=subevents
|
||||
subevent=subevent
|
||||
)
|
||||
orders_to_change = orders_to_cancel.annotate(
|
||||
has_subevent=Exists(has_subevent),
|
||||
@@ -131,18 +124,15 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
|
||||
has_subevent=True, has_other_subevent=False
|
||||
)
|
||||
|
||||
for se in subevents:
|
||||
se.log_action(
|
||||
'pretix.subevent.canceled', user=user,
|
||||
)
|
||||
se.active = False
|
||||
se.save(update_fields=['active'])
|
||||
se.log_action(
|
||||
'pretix.subevent.changed', user=user, data={'active': False, '_source': 'cancel_event'}
|
||||
)
|
||||
subevent.log_action(
|
||||
'pretix.subevent.canceled', user=user,
|
||||
)
|
||||
subevent.active = False
|
||||
subevent.save(update_fields=['active'])
|
||||
subevent.log_action(
|
||||
'pretix.subevent.changed', user=user, data={'active': False, '_source': 'cancel_event'}
|
||||
)
|
||||
else:
|
||||
subevents = None
|
||||
subevent_ids = set()
|
||||
orders_to_change = event.orders.none()
|
||||
event.log_action(
|
||||
'pretix.event.canceled', user=user,
|
||||
@@ -156,9 +146,7 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
|
||||
)
|
||||
failed = 0
|
||||
total = orders_to_cancel.count() + orders_to_change.count()
|
||||
qs_wl = event.waitinglistentries.filter(voucher__isnull=True).select_related('subevent')
|
||||
if subevents:
|
||||
qs_wl = qs_wl.filter(subevent__in=subevents)
|
||||
qs_wl = event.waitinglistentries.filter(subevent=subevent, voucher__isnull=True)
|
||||
if send_waitinglist:
|
||||
total += qs_wl.count()
|
||||
counter = 0
|
||||
@@ -182,10 +170,6 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
|
||||
fee += Decimal(keep_fee_percentage) / Decimal('100.00') * (o.total - fee_sum)
|
||||
if 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)
|
||||
|
||||
_cancel_order(o.pk, user, send_mail=False, cancellation_fee=fee, keep_fees=keep_fee_objects)
|
||||
@@ -217,20 +201,16 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
|
||||
with transaction.atomic():
|
||||
o = event.orders.select_for_update().get(pk=o)
|
||||
total = Decimal('0.00')
|
||||
fee = Decimal('0.00')
|
||||
positions = []
|
||||
|
||||
ocm = OrderChangeManager(o, user=user, notify=False)
|
||||
for p in o.positions.all():
|
||||
if p.subevent_id in subevent_ids:
|
||||
if p.subevent == subevent:
|
||||
total += p.price
|
||||
ocm.cancel(p)
|
||||
positions.append(p)
|
||||
|
||||
if keep_fee_per_ticket:
|
||||
if p.addon_to_id is None:
|
||||
fee += min(p.price, Decimal(keep_fee_per_ticket))
|
||||
|
||||
fee = Decimal('0.00')
|
||||
if keep_fee_fixed:
|
||||
fee += Decimal(keep_fee_fixed)
|
||||
if keep_fee_percentage:
|
||||
@@ -266,7 +246,7 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
|
||||
|
||||
if send_waitinglist:
|
||||
for wle in qs_wl:
|
||||
_send_wle_mail(wle, send_waitinglist_subject, send_waitinglist_message, wle.subevent)
|
||||
_send_wle_mail(wle, send_waitinglist_subject, send_waitinglist_message, subevent)
|
||||
|
||||
counter += 1
|
||||
if not self.request.called_directly and counter % max(10, total // 100) == 0:
|
||||
|
||||
@@ -106,7 +106,6 @@ error_messages = {
|
||||
'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.'),
|
||||
'gift_card': _("You entered a gift card instead of a voucher. Gift cards can be entered later on when you're asked for your payment details."),
|
||||
'country_blocked': _('One of the selected products is not available in the selected country.'),
|
||||
}
|
||||
|
||||
|
||||
@@ -325,8 +324,6 @@ class CartManager:
|
||||
custom_price_is_net=cp_is_net if cp_is_net is not None else self.event.settings.display_net_prices,
|
||||
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:
|
||||
if str(e) == 'price_too_high':
|
||||
raise CartError(error_messages['price_too_high'])
|
||||
@@ -1066,7 +1063,6 @@ def update_tax_rates(event: Event, cart_id: str, invoice_address: InvoiceAddress
|
||||
if pos.tax_rate != rate:
|
||||
current_net = pos.price - pos.tax_value
|
||||
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.includes_tax = rate != Decimal('0.00')
|
||||
pos.override_tax_rate = rate
|
||||
|
||||
@@ -7,12 +7,11 @@ from django.dispatch import receiver
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now, override
|
||||
from django.utils.translation import gettext as _
|
||||
from django_scopes import scope, scopes_disabled
|
||||
|
||||
from pretix.base.models import (
|
||||
Checkin, CheckinList, Device, Order, OrderPosition, QuestionOption,
|
||||
)
|
||||
from pretix.base.signals import checkin_created, order_placed, periodic_task
|
||||
from pretix.base.signals import checkin_created, order_placed
|
||||
from pretix.helpers.jsonlogic import Logic
|
||||
|
||||
|
||||
@@ -230,7 +229,6 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
||||
list=clist,
|
||||
datetime=dt,
|
||||
device=device,
|
||||
gate=device.gate if device else None,
|
||||
nonce=nonce,
|
||||
forced=force and not entry_allowed,
|
||||
)
|
||||
@@ -264,23 +262,5 @@ def order_placed(sender, **kwargs):
|
||||
for cl in cls:
|
||||
if cl.all_products or op.item_id in {i.pk for i in cl.limit_products.all()}:
|
||||
if not cl.subevent_id or cl.subevent_id == op.subevent_id:
|
||||
ci = Checkin.objects.create(position=op, list=cl, auto_checked_in=True, type=Checkin.TYPE_ENTRY)
|
||||
ci = Checkin.objects.create(position=op, list=cl, auto_checked_in=True)
|
||||
checkin_created.send(event, checkin=ci)
|
||||
|
||||
|
||||
@receiver(periodic_task, dispatch_uid="autocheckin_exit_all")
|
||||
@scopes_disabled()
|
||||
def process_exit_all(sender, **kwargs):
|
||||
qs = CheckinList.objects.filter(
|
||||
exit_all_at__lte=now(),
|
||||
exit_all_at__isnull=False
|
||||
).select_related('event', 'event__organizer')
|
||||
for cl in qs:
|
||||
for p in cl.positions_inside:
|
||||
with scope(organizer=cl.event.organizer):
|
||||
ci = Checkin.objects.create(
|
||||
position=p, list=cl, auto_checked_in=True, type=Checkin.TYPE_EXIT, datetime=cl.exit_all_at
|
||||
)
|
||||
checkin_created.send(cl.event, checkin=ci)
|
||||
cl.exit_all_at = cl.exit_all_at + timedelta(days=1)
|
||||
cl.save(update_fields=['exit_all_at'])
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files.base import ContentFile
|
||||
from django.utils.timezone import override
|
||||
from django.utils.translation import gettext
|
||||
|
||||
from pretix.base.i18n import LazyLocaleException, language
|
||||
from pretix.base.models import (
|
||||
CachedFile, Device, Event, Organizer, TeamAPIToken, User, cachedfile_name,
|
||||
CachedFile, Event, Organizer, User, cachedfile_name,
|
||||
)
|
||||
from pretix.base.services.tasks import (
|
||||
ProfiledEventTask, ProfiledOrganizerUserTask,
|
||||
@@ -49,13 +48,7 @@ def export(self, event: Event, fileid: str, provider: str, form_data: Dict[str,
|
||||
|
||||
|
||||
@app.task(base=ProfiledOrganizerUserTask, throws=(ExportError,), bind=True)
|
||||
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 multiexport(self, organizer: Organizer, user: User, fileid: str, provider: str, form_data: Dict[str, Any]) -> None:
|
||||
def set_progress(val):
|
||||
if not self.request.called_directly:
|
||||
self.update_state(
|
||||
@@ -64,22 +57,10 @@ def multiexport(self, organizer: Organizer, user: User, device: int, token: int,
|
||||
)
|
||||
|
||||
file = CachedFile.objects.get(id=fileid)
|
||||
if user:
|
||||
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'))
|
||||
with language(user.locale), override(user.timezone):
|
||||
allowed_events = user.get_events_with_permission('can_view_orders')
|
||||
|
||||
events = allowed_events.filter(pk__in=form_data.get('events'))
|
||||
responses = register_multievent_data_exporters.send(organizer)
|
||||
|
||||
for receiver, response in responses:
|
||||
|
||||
@@ -24,7 +24,7 @@ from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
Invoice, InvoiceAddress, InvoiceLine, Order, OrderFee,
|
||||
)
|
||||
from pretix.base.models.tax import EU_CURRENCIES, is_eu_country
|
||||
from pretix.base.models.tax import EU_COUNTRIES, EU_CURRENCIES
|
||||
from pretix.base.services.tasks import TransactionAwareTask
|
||||
from pretix.base.settings import GlobalSettingsObject
|
||||
from pretix.base.signals import invoice_line_text, periodic_task
|
||||
@@ -181,7 +181,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
||||
if reverse_charge:
|
||||
if invoice.additional_text:
|
||||
invoice.additional_text += "<br /><br />"
|
||||
if is_eu_country(invoice.invoice_to_country):
|
||||
if str(invoice.invoice_to_country) in EU_COUNTRIES:
|
||||
invoice.additional_text += pgettext(
|
||||
"invoice",
|
||||
"Reverse Charge: According to Article 194, 196 of Council Directive 2006/112/EEC, VAT liability "
|
||||
|
||||
@@ -231,7 +231,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
||||
attach_tickets=attach_tickets,
|
||||
attach_ical=attach_ical,
|
||||
user=user.pk if user else None,
|
||||
attach_cached_files=[(cf.id if isinstance(cf, CachedFile) else cf) for cf in attach_cached_files] if attach_cached_files else [],
|
||||
attach_cached_files=[cf.id for cf in attach_cached_files] if attach_cached_files else [],
|
||||
)
|
||||
|
||||
if invoices:
|
||||
@@ -372,7 +372,7 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
|
||||
backend.send_messages([email])
|
||||
except smtplib.SMTPResponseException as e:
|
||||
if e.smtp_code in (101, 111, 421, 422, 431, 442, 447, 452):
|
||||
self.retry(max_retries=5, countdown=2 ** (self.request.retries * 3)) # max is 2 ** (4*3) = 4096 seconds = 68 minutes
|
||||
self.retry(max_retries=5, countdown=2 ** (self.request.retries * 2))
|
||||
logger.exception('Error sending email')
|
||||
|
||||
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))
|
||||
except Exception as e:
|
||||
if isinstance(e, (smtplib.SMTPServerDisconnected, smtplib.SMTPConnectError, ssl.SSLError, OSError)):
|
||||
self.retry(max_retries=5, countdown=2 ** (self.request.retries * 3)) # max is 2 ** (4*3) = 4096 seconds = 68 minutes
|
||||
self.retry(max_retries=5, countdown=2 ** (self.request.retries * 2))
|
||||
if order:
|
||||
order.log_action(
|
||||
'pretix.event.order.email.error',
|
||||
|
||||
@@ -15,59 +15,55 @@ from pretix.helpers.urls import build_absolute_uri
|
||||
|
||||
@app.task(base=TransactionAwareTask, acks_late=True)
|
||||
@scopes_disabled()
|
||||
def notify(logentry_ids: list):
|
||||
if not isinstance(logentry_ids, list):
|
||||
logentry_ids = [logentry_ids]
|
||||
def notify(logentry_id: int):
|
||||
logentry = LogEntry.all.select_related('event', 'event__organizer').get(id=logentry_id)
|
||||
if not logentry.event:
|
||||
return # Ignore, we only have event-related notifications right now
|
||||
types = get_all_notification_types(logentry.event)
|
||||
|
||||
qs = LogEntry.all.select_related('event', 'event__organizer').filter(id__in=logentry_ids)
|
||||
notification_type = None
|
||||
typepath = logentry.action_type
|
||||
while not notification_type and '.' in typepath:
|
||||
notification_type = types.get(typepath + ('.*' if typepath != logentry.action_type else ''))
|
||||
typepath = typepath.rsplit('.', 1)[0]
|
||||
|
||||
_event, _at, notify_specific, notify_global = None, None, None, None
|
||||
for logentry in qs:
|
||||
if not logentry.event:
|
||||
break # Ignore, we only have event-related notifications right now
|
||||
if not notification_type:
|
||||
return # No suitable plugin
|
||||
|
||||
notification_type = logentry.notification_type
|
||||
# All users that have the permission to get the notification
|
||||
users = logentry.event.get_users_with_permission(
|
||||
notification_type.required_permission
|
||||
).filter(notifications_send=True, is_active=True)
|
||||
if logentry.user:
|
||||
users = users.exclude(pk=logentry.user.pk)
|
||||
|
||||
if not notification_type:
|
||||
break # No suitable plugin
|
||||
# Get all notification settings, both specific to this event as well as global
|
||||
notify_specific = {
|
||||
(ns.user, ns.method): ns.enabled
|
||||
for ns in NotificationSetting.objects.filter(
|
||||
event=logentry.event,
|
||||
action_type=notification_type.action_type,
|
||||
user__pk__in=users.values_list('pk', flat=True)
|
||||
)
|
||||
}
|
||||
notify_global = {
|
||||
(ns.user, ns.method): ns.enabled
|
||||
for ns in NotificationSetting.objects.filter(
|
||||
event__isnull=True,
|
||||
action_type=notification_type.action_type,
|
||||
user__pk__in=users.values_list('pk', flat=True)
|
||||
)
|
||||
}
|
||||
|
||||
if _event != logentry.event or _at != logentry.action_type or notify_global is None:
|
||||
_event = logentry.event
|
||||
_at = logentry.action_type
|
||||
# All users that have the permission to get the notification
|
||||
users = logentry.event.get_users_with_permission(
|
||||
notification_type.required_permission
|
||||
).filter(notifications_send=True, is_active=True)
|
||||
if logentry.user:
|
||||
users = users.exclude(pk=logentry.user.pk)
|
||||
for um, enabled in notify_specific.items():
|
||||
user, method = um
|
||||
if enabled:
|
||||
send_notification.apply_async(args=(logentry_id, notification_type.action_type, user.pk, method))
|
||||
|
||||
# Get all notification settings, both specific to this event as well as global
|
||||
notify_specific = {
|
||||
(ns.user, ns.method): ns.enabled
|
||||
for ns in NotificationSetting.objects.filter(
|
||||
event=logentry.event,
|
||||
action_type=notification_type.action_type,
|
||||
user__pk__in=users.values_list('pk', flat=True)
|
||||
)
|
||||
}
|
||||
notify_global = {
|
||||
(ns.user, ns.method): ns.enabled
|
||||
for ns in NotificationSetting.objects.filter(
|
||||
event__isnull=True,
|
||||
action_type=notification_type.action_type,
|
||||
user__pk__in=users.values_list('pk', flat=True)
|
||||
)
|
||||
}
|
||||
|
||||
for um, enabled in notify_specific.items():
|
||||
user, method = um
|
||||
if enabled:
|
||||
send_notification.apply_async(args=(logentry.id, notification_type.action_type, user.pk, method))
|
||||
|
||||
for um, enabled in notify_global.items():
|
||||
user, method = um
|
||||
if enabled and um not in notify_specific:
|
||||
send_notification.apply_async(args=(logentry.id, notification_type.action_type, user.pk, method))
|
||||
for um, enabled in notify_global.items():
|
||||
user, method = um
|
||||
if enabled and um not in notify_specific:
|
||||
send_notification.apply_async(args=(logentry_id, notification_type.action_type, user.pk, method))
|
||||
|
||||
|
||||
@app.task(base=ProfiledTask, acks_late=True)
|
||||
|
||||
@@ -32,13 +32,13 @@ from pretix.base.models import (
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.items import ItemBundle
|
||||
from pretix.base.models.orders import (
|
||||
InvoiceAddress, OrderFee, OrderRefund, generate_secret,
|
||||
InvoiceAddress, OrderFee, OrderRefund, generate_position_secret,
|
||||
generate_secret,
|
||||
)
|
||||
from pretix.base.models.organizer import TeamAPIToken
|
||||
from pretix.base.models.tax import TaxRule
|
||||
from pretix.base.payment import BasePaymentProvider, PaymentException
|
||||
from pretix.base.reldate import RelativeDateWrapper
|
||||
from pretix.base.secrets import assign_ticket_secret
|
||||
from pretix.base.services import tickets
|
||||
from pretix.base.services.invoices import (
|
||||
generate_cancellation, generate_invoice, invoice_qualified,
|
||||
@@ -89,7 +89,6 @@ error_messages = {
|
||||
'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_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__)
|
||||
@@ -372,10 +371,7 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
|
||||
if position.voucher:
|
||||
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
|
||||
position.canceled = True
|
||||
assign_ticket_secret(
|
||||
event=order.event, position=position, force_invalidate_if_revokation_list_used=True, force_invalidate=False, save=False
|
||||
)
|
||||
position.save(update_fields=['canceled', 'secret'])
|
||||
position.save(update_fields=['canceled'])
|
||||
new_fee = cancellation_fee
|
||||
for fee in order.fees.all():
|
||||
if keep_fees and fee in keep_fees:
|
||||
@@ -410,9 +406,6 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
|
||||
order.save(update_fields=['status', 'cancellation_date'])
|
||||
|
||||
for position in order.positions.all():
|
||||
assign_ticket_secret(
|
||||
event=order.event, position=position, force_invalidate_if_revokation_list_used=True, force_invalidate=False, save=True
|
||||
)
|
||||
if position.voucher:
|
||||
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
|
||||
|
||||
@@ -616,39 +609,34 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
current_discount = cp.price_before_voucher - cp.price
|
||||
max_discount = max(v_budget[cp.voucher] + current_discount, 0)
|
||||
|
||||
try:
|
||||
if cp.is_bundled:
|
||||
try:
|
||||
bundle = cp.addon_to.item.bundles.get(bundled_item=cp.item, bundled_variation=cp.variation)
|
||||
bprice = bundle.designated_price or 0
|
||||
except ItemBundle.DoesNotExist:
|
||||
bprice = cp.price
|
||||
except ItemBundle.MultipleObjectsReturned:
|
||||
raise OrderError("Invalid product configuration (duplicate bundle)")
|
||||
price = get_price(cp.item, cp.variation, cp.voucher, bprice, cp.subevent, custom_price_is_net=False,
|
||||
custom_price_is_tax_rate=cp.override_tax_rate,
|
||||
invoice_address=address, force_custom_price=True, max_discount=max_discount)
|
||||
pbv = get_price(cp.item, cp.variation, None, bprice, cp.subevent, custom_price_is_net=False,
|
||||
custom_price_is_tax_rate=cp.override_tax_rate,
|
||||
invoice_address=address, force_custom_price=True, max_discount=max_discount)
|
||||
changed_prices[cp.pk] = bprice
|
||||
else:
|
||||
bundled_sum = 0
|
||||
if not cp.addon_to_id:
|
||||
for bundledp in cp.addons.all():
|
||||
if bundledp.is_bundled:
|
||||
bundled_sum += changed_prices.get(bundledp.pk, bundledp.price)
|
||||
if cp.is_bundled:
|
||||
try:
|
||||
bundle = cp.addon_to.item.bundles.get(bundled_item=cp.item, bundled_variation=cp.variation)
|
||||
bprice = bundle.designated_price or 0
|
||||
except ItemBundle.DoesNotExist:
|
||||
bprice = cp.price
|
||||
except ItemBundle.MultipleObjectsReturned:
|
||||
raise OrderError("Invalid product configuration (duplicate bundle)")
|
||||
price = get_price(cp.item, cp.variation, cp.voucher, bprice, cp.subevent, custom_price_is_net=False,
|
||||
custom_price_is_tax_rate=cp.override_tax_rate,
|
||||
invoice_address=address, force_custom_price=True, max_discount=max_discount)
|
||||
pbv = get_price(cp.item, cp.variation, None, bprice, cp.subevent, custom_price_is_net=False,
|
||||
custom_price_is_tax_rate=cp.override_tax_rate,
|
||||
invoice_address=address, force_custom_price=True, max_discount=max_discount)
|
||||
changed_prices[cp.pk] = bprice
|
||||
else:
|
||||
bundled_sum = 0
|
||||
if not cp.addon_to_id:
|
||||
for bundledp in cp.addons.all():
|
||||
if bundledp.is_bundled:
|
||||
bundled_sum += changed_prices.get(bundledp.pk, bundledp.price)
|
||||
|
||||
price = get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent, custom_price_is_net=False,
|
||||
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)
|
||||
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,
|
||||
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
|
||||
price = get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent, custom_price_is_net=False,
|
||||
addon_to=cp.addon_to, invoice_address=address, bundled_sum=bundled_sum,
|
||||
max_discount=max_discount, custom_price_is_tax_rate=cp.override_tax_rate)
|
||||
pbv = get_price(cp.item, cp.variation, None, cp.price, cp.subevent, custom_price_is_net=False,
|
||||
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)
|
||||
|
||||
if max_discount is not None:
|
||||
v_budget[cp.voucher] = v_budget[cp.voucher] + current_discount - (pbv.gross - price.gross)
|
||||
@@ -1180,7 +1168,6 @@ class OrderChangeManager:
|
||||
'seat_subevent_mismatch': _('You selected seat "{seat}" for a date that does not match the selected ticket date. Please choose a seat again.'),
|
||||
'seat_required': _('The selected product requires you 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.'),
|
||||
}
|
||||
ItemOperation = namedtuple('ItemOperation', ('position', 'item', 'variation'))
|
||||
@@ -1248,11 +1235,8 @@ class OrderChangeManager:
|
||||
self._operations.append(self.SeatOperation(position, seat))
|
||||
|
||||
def change_subevent(self, position: OrderPosition, subevent: SubEvent):
|
||||
try:
|
||||
price = get_price(position.item, position.variation, voucher=position.voucher, subevent=subevent,
|
||||
invoice_address=self._invoice_address)
|
||||
except TaxRule.SaleNotAllowed:
|
||||
raise OrderError(self.error_messages['tax_rule_country_blocked'])
|
||||
price = get_price(position.item, position.variation, voucher=position.voucher, subevent=subevent,
|
||||
invoice_address=self._invoice_address)
|
||||
|
||||
if price is None: # NOQA
|
||||
raise OrderError(self.error_messages['product_invalid'])
|
||||
@@ -1272,11 +1256,8 @@ class OrderChangeManager:
|
||||
if (not variation and item.has_variations) or (variation and variation.item_id != item.pk):
|
||||
raise OrderError(self.error_messages['product_without_variation'])
|
||||
|
||||
try:
|
||||
price = get_price(item, variation, voucher=position.voucher, subevent=subevent,
|
||||
invoice_address=self._invoice_address)
|
||||
except TaxRule.SaleNotAllowed:
|
||||
raise OrderError(self.error_messages['tax_rule_country_blocked'])
|
||||
price = get_price(item, variation, voucher=position.voucher, subevent=subevent,
|
||||
invoice_address=self._invoice_address)
|
||||
|
||||
if price is None: # NOQA
|
||||
raise OrderError(self.error_messages['product_invalid'])
|
||||
@@ -1334,10 +1315,7 @@ class OrderChangeManager:
|
||||
if not pos.price:
|
||||
continue
|
||||
|
||||
try:
|
||||
new_rate = tax_rule.tax_rate_for(ia)
|
||||
except TaxRule.SaleNotAllowed:
|
||||
raise OrderError(error_messages['tax_rule_country_blocked'])
|
||||
new_rate = tax_rule.tax_rate_for(ia)
|
||||
# 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 keep == 'net':
|
||||
@@ -1390,13 +1368,10 @@ class OrderChangeManager:
|
||||
except Seat.DoesNotExist:
|
||||
raise OrderError(error_messages['seat_invalid'])
|
||||
|
||||
try:
|
||||
if price is None:
|
||||
price = get_price(item, variation, subevent=subevent, invoice_address=self._invoice_address)
|
||||
else:
|
||||
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:
|
||||
price = get_price(item, variation, subevent=subevent, invoice_address=self._invoice_address)
|
||||
else:
|
||||
price = item.tax(price, base_price_is='gross', invoice_address=self._invoice_address)
|
||||
|
||||
if price is None:
|
||||
raise OrderError(self.error_messages['product_invalid'])
|
||||
@@ -1589,9 +1564,6 @@ class OrderChangeManager:
|
||||
invoice_address=self._invoice_address
|
||||
).gross
|
||||
)
|
||||
assign_ticket_secret(
|
||||
event=self.event, position=op.position, force_invalidate=False, save=False
|
||||
)
|
||||
op.position.save()
|
||||
elif isinstance(op, self.SeatOperation):
|
||||
self.order.log_action('pretix.event.order.changed.seat', user=self.user, auth=self.auth, data={
|
||||
@@ -1603,9 +1575,6 @@ class OrderChangeManager:
|
||||
'new_seat_id': op.seat.pk if op.seat else None,
|
||||
})
|
||||
op.position.seat = op.seat
|
||||
assign_ticket_secret(
|
||||
event=self.event, position=op.position, force_invalidate=False, save=False
|
||||
)
|
||||
op.position.save()
|
||||
elif isinstance(op, self.SubeventOperation):
|
||||
self.order.log_action('pretix.event.order.changed.subevent', user=self.user, auth=self.auth, data={
|
||||
@@ -1617,9 +1586,7 @@ class OrderChangeManager:
|
||||
'new_price': op.position.price
|
||||
})
|
||||
op.position.subevent = op.subevent
|
||||
assign_ticket_secret(
|
||||
event=self.event, position=op.position, force_invalidate=False, save=False
|
||||
)
|
||||
op.position.save()
|
||||
if op.position.price_before_voucher is not None and op.position.voucher and not op.position.addon_to_id:
|
||||
op.position.price_before_voucher = max(
|
||||
op.position.price,
|
||||
@@ -1630,7 +1597,6 @@ class OrderChangeManager:
|
||||
invoice_address=self._invoice_address
|
||||
).gross
|
||||
)
|
||||
op.position.save()
|
||||
elif isinstance(op, self.AddFeeOperation):
|
||||
self.order.log_action('pretix.event.order.changed.addfee', user=self.user, auth=self.auth, data={
|
||||
'fee': op.fee.pk,
|
||||
@@ -1709,10 +1675,7 @@ class OrderChangeManager:
|
||||
opa.canceled = True
|
||||
if opa.voucher:
|
||||
Voucher.objects.filter(pk=opa.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
|
||||
assign_ticket_secret(
|
||||
event=self.event, position=op.position, force_invalidate_if_revokation_list_used=True, force_invalidate=False, save=False
|
||||
)
|
||||
opa.save(update_fields=['canceled', 'secret'])
|
||||
opa.save(update_fields=['canceled'])
|
||||
self.order.log_action('pretix.event.order.changed.cancel', user=self.user, auth=self.auth, data={
|
||||
'position': op.position.pk,
|
||||
'positionid': op.position.positionid,
|
||||
@@ -1724,10 +1687,7 @@ class OrderChangeManager:
|
||||
op.position.canceled = True
|
||||
if op.position.voucher:
|
||||
Voucher.objects.filter(pk=op.position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
|
||||
assign_ticket_secret(
|
||||
event=self.event, position=op.position, force_invalidate_if_revokation_list_used=True, force_invalidate=False, save=False
|
||||
)
|
||||
op.position.save(update_fields=['canceled', 'secret'])
|
||||
op.position.save(update_fields=['canceled'])
|
||||
elif isinstance(op, self.AddOperation):
|
||||
pos = OrderPosition.objects.create(
|
||||
item=op.item, variation=op.variation, addon_to=op.addon_to,
|
||||
@@ -1749,9 +1709,8 @@ class OrderChangeManager:
|
||||
elif isinstance(op, self.SplitOperation):
|
||||
split_positions.append(op.position)
|
||||
elif isinstance(op, self.RegenerateSecretOperation):
|
||||
assign_ticket_secret(
|
||||
event=self.event, position=op.position, force_invalidate=True, save=True
|
||||
)
|
||||
op.position.secret = generate_position_secret()
|
||||
op.position.save()
|
||||
tickets.invalidate_cache.apply_async(kwargs={'event': self.event.pk,
|
||||
'order': self.order.pk})
|
||||
self.order.log_action('pretix.event.order.changed.secret', user=self.user, auth=self.auth, data={
|
||||
@@ -1784,9 +1743,7 @@ class OrderChangeManager:
|
||||
'new_order': split_order.code,
|
||||
})
|
||||
op.order = split_order
|
||||
assign_ticket_secret(
|
||||
self.event, position=op, force_invalidate=True,
|
||||
)
|
||||
op.secret = generate_position_secret()
|
||||
op.save()
|
||||
|
||||
try:
|
||||
@@ -1971,10 +1928,7 @@ class OrderChangeManager:
|
||||
self._check_quotas()
|
||||
self._check_seats()
|
||||
self._check_complete_cancel()
|
||||
try:
|
||||
self._perform_operations()
|
||||
except TaxRule.SaleNotAllowed:
|
||||
raise OrderError(self.error_messages['tax_rule_country_blocked'])
|
||||
self._perform_operations()
|
||||
self._recalculate_total_and_payment_fee()
|
||||
self._reissue_invoice()
|
||||
self._clear_tickets_cache()
|
||||
|
||||
@@ -17,7 +17,7 @@ from pretix.celery_app import app
|
||||
|
||||
|
||||
@app.task(base=ProfiledEventTask)
|
||||
def export(event: Event, shredders: List[str], session_key=None) -> None:
|
||||
def export(event: Event, shredders: List[str]) -> None:
|
||||
known_shredders = event.get_data_shredders()
|
||||
|
||||
with NamedTemporaryFile() as rawfile:
|
||||
@@ -55,8 +55,6 @@ def export(event: Event, shredders: List[str], session_key=None) -> None:
|
||||
cf.date = now()
|
||||
cf.filename = event.slug + '.zip'
|
||||
cf.type = 'application/zip'
|
||||
cf.session_key = session_key
|
||||
cf.web_download = True
|
||||
cf.expires = now() + timedelta(hours=1)
|
||||
cf.save()
|
||||
cf.file.save(cachedfile_name(cf, cf.filename), rawfile)
|
||||
|
||||
@@ -127,7 +127,6 @@ def order_overview(
|
||||
order__event=event
|
||||
).annotate(
|
||||
status=Case(
|
||||
When(order__status='n', order__require_approval=True, then=Value('unapproved')),
|
||||
When(canceled=True, then=Value('c')),
|
||||
default=F('order__status')
|
||||
)
|
||||
@@ -136,7 +135,6 @@ def order_overview(
|
||||
).annotate(cnt=Count('id'), price=Sum('price'), tax_value=Sum('tax_value')).order_by()
|
||||
|
||||
states = {
|
||||
'unapproved': 'unapproved',
|
||||
'canceled': Order.STATUS_CANCELED,
|
||||
'paid': Order.STATUS_PAID,
|
||||
'pending': Order.STATUS_PENDING,
|
||||
@@ -200,7 +198,6 @@ def order_overview(
|
||||
order__event=event
|
||||
).annotate(
|
||||
status=Case(
|
||||
When(order__status='n', order__require_approval=True, then=Value('unapproved')),
|
||||
When(canceled=True, then=Value('c')),
|
||||
default=F('order__status')
|
||||
)
|
||||
|
||||
@@ -96,9 +96,8 @@ class OrganizerUserTask(app.Task):
|
||||
kwargs['organizer'] = organizer
|
||||
|
||||
user_id = kwargs['user']
|
||||
if user_id is not None:
|
||||
user = User.objects.get(pk=user_id)
|
||||
kwargs['user'] = user
|
||||
user = User.objects.get(pk=user_id)
|
||||
kwargs['user'] = user
|
||||
|
||||
with scope(organizer=organizer):
|
||||
ret = super().__call__(*args, **kwargs)
|
||||
|
||||
@@ -374,10 +374,6 @@ DEFAULTS = {
|
||||
'default': 'classic',
|
||||
'type': str,
|
||||
},
|
||||
'ticket_secret_generator': {
|
||||
'default': 'random',
|
||||
'type': str,
|
||||
},
|
||||
'reservation_time': {
|
||||
'default': '30',
|
||||
'type': int,
|
||||
@@ -975,19 +971,6 @@ DEFAULTS = {
|
||||
'data-checkbox-dependency-visual': 'on'}),
|
||||
)
|
||||
},
|
||||
'ticket_download_require_validated_email': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_class': forms.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Do not issue ticket before email address is validated"),
|
||||
help_text=_("If turned on, tickets will not be offered for download directly after purchase. They will "
|
||||
"be attached to the payment confirmation email (if the file size is not too large), and the "
|
||||
"customer will be able to download them from the page as soon as they clicked a link in "
|
||||
"the email. Does not affect orders performed through other sales channels."),
|
||||
)
|
||||
},
|
||||
'event_list_availability': {
|
||||
'default': 'True',
|
||||
'type': bool
|
||||
@@ -2049,7 +2032,7 @@ PERSON_NAME_SCHEMES = OrderedDict([
|
||||
'title': pgettext_lazy('person_name_sample', 'Dr'),
|
||||
'given_name': pgettext_lazy('person_name_sample', 'John'),
|
||||
'family_name': pgettext_lazy('person_name_sample', 'Doe'),
|
||||
'_scheme': 'salutation_title_given_family',
|
||||
'_scheme': 'title_salutation_given_family',
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
@@ -216,16 +216,6 @@ subclass of pretix.base.invoice.BaseInvoiceRenderer or a list of these
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
register_ticket_secret_generators = EventPluginSignal(
|
||||
providing_args=[]
|
||||
)
|
||||
"""
|
||||
This signal is sent out to get all known ticket secret generators. Receivers should return a
|
||||
subclass of ``pretix.base.secrets.BaseTicketSecretGenerator`` or a list of these
|
||||
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
register_data_shredders = EventPluginSignal(
|
||||
providing_args=[]
|
||||
)
|
||||
|
||||
@@ -4,4 +4,3 @@
|
||||
<label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}</label>{% endif %}{% if widget.value.is_img %}<br><a href="{{ widget.value.url }}" data-lightbox="{{ widget.value.name }}"><img src="{{ widget.value|thumb:"200x100" }}" /></a>{% endif %}<br>
|
||||
{{ widget.input_text }}:{% endif %}
|
||||
<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 %}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user