mirror of
https://github.com/pretix/pretix.git
synced 2025-12-10 01:12:28 +00:00
Compare commits
176 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d473f56c3a | ||
|
|
4138ab3d7d | ||
|
|
e18d1a451d | ||
|
|
a3048cd393 | ||
|
|
dd8fdc6c0a | ||
|
|
9099e4b709 | ||
|
|
52b176b9eb | ||
|
|
69fd70787c | ||
|
|
ff37aea9c8 | ||
|
|
85f73977bf | ||
|
|
2c04ed48c2 | ||
|
|
1228754280 | ||
|
|
a43ee054ad | ||
|
|
83bc714739 | ||
|
|
a08390c84a | ||
|
|
8b6eacecfe | ||
|
|
fb96787697 | ||
|
|
9cff77be62 | ||
|
|
0d1643da66 | ||
|
|
5e7027647a | ||
|
|
28f6f09e8f | ||
|
|
332af5d21b | ||
|
|
e187005130 | ||
|
|
0357386f7c | ||
|
|
47f8e5b8c6 | ||
|
|
e95c9d73a1 | ||
|
|
b7174070fe | ||
|
|
dd06a7b62c | ||
|
|
ff9d480b6e | ||
|
|
229ad9108b | ||
|
|
0e332d291a | ||
|
|
180904cdc2 | ||
|
|
0e83f7d807 | ||
|
|
5d7931fcaf | ||
|
|
2e906b0bf5 | ||
|
|
33ae6f12de | ||
|
|
f302c2e154 | ||
|
|
3ee2492382 | ||
|
|
4caed50018 | ||
|
|
aadb19a792 | ||
|
|
9f8211a873 | ||
|
|
d45fc05e5d | ||
|
|
955a3a054e | ||
|
|
60f265a5fa | ||
|
|
a2d82a1a7b | ||
|
|
0875d728e8 | ||
|
|
f3cf6b8b38 | ||
|
|
e4465cffb0 | ||
|
|
ca35d714dc | ||
|
|
c06e7348c4 | ||
|
|
60ac8a6ebd | ||
|
|
e3450baeb3 | ||
|
|
72661623f3 | ||
|
|
b4d97d9432 | ||
|
|
b40100f78b | ||
|
|
a343d2b42c | ||
|
|
d3d7e54cff | ||
|
|
6535bc3d5e | ||
|
|
f966fc8d84 | ||
|
|
8a20bbd943 | ||
|
|
cd0f6d85ba | ||
|
|
d51edbb3bb | ||
|
|
553e475cfb | ||
|
|
b9367446d9 | ||
|
|
82d9fccec8 | ||
|
|
cbbcfb7a3a | ||
|
|
1f862b27c1 | ||
|
|
883b03349e | ||
|
|
f740a6ba61 | ||
|
|
fb3e761a37 | ||
|
|
3c7411328d | ||
|
|
9c2bfdfead | ||
|
|
4f3bd1ff4a | ||
|
|
69d10489b8 | ||
|
|
df031b2222 | ||
|
|
850b9e5e3d | ||
|
|
a95a208e1b | ||
|
|
50ff3628f7 | ||
|
|
14d203055b | ||
|
|
4628e28592 | ||
|
|
7fb3d13733 | ||
|
|
11ff81f852 | ||
|
|
0f5af4b990 | ||
|
|
85420602e8 | ||
|
|
6ccf55b601 | ||
|
|
42c9e21d04 | ||
|
|
3030c300f2 | ||
|
|
48b969f3c3 | ||
|
|
bbb78aa5e6 | ||
|
|
31380bbef2 | ||
|
|
479a7d9162 | ||
|
|
6fe02f156a | ||
|
|
c4ed210fed | ||
|
|
ae686fab38 | ||
|
|
8edca9ed5d | ||
|
|
05bafd0db5 | ||
|
|
341d699240 | ||
|
|
552093d962 | ||
|
|
eb6063cc2d | ||
|
|
550ff4ff18 | ||
|
|
5383a8b77c | ||
|
|
86117091fe | ||
|
|
b113028a5f | ||
|
|
60a3f21857 | ||
|
|
65a2ea3935 | ||
|
|
6ecddfc6c0 | ||
|
|
d65d48db48 | ||
|
|
f509b26800 | ||
|
|
43fb6fe6e5 | ||
|
|
9d2d8684b6 | ||
|
|
1689925508 | ||
|
|
4d249553bf | ||
|
|
43ea1044cd | ||
|
|
cc4a301dc1 | ||
|
|
ab67eea36e | ||
|
|
fa326eba6f | ||
|
|
c30ebdf287 | ||
|
|
835bcb7207 | ||
|
|
777424ad18 | ||
|
|
4985e7e96d | ||
|
|
ca1e64ec10 | ||
|
|
26029508c6 | ||
|
|
118259a96b | ||
|
|
35e8dcf2bc | ||
|
|
359a5d01e6 | ||
|
|
1c2acbb57f | ||
|
|
01a702c529 | ||
|
|
1ee584c5a1 | ||
|
|
fc10bd7749 | ||
|
|
f2568092a7 | ||
|
|
6b5d5a6334 | ||
|
|
195ed57025 | ||
|
|
008b4a134b | ||
|
|
1b9bfb5b62 | ||
|
|
edeaa1333b | ||
|
|
e678b52a7e | ||
|
|
b549db58e4 | ||
|
|
c14059f66a | ||
|
|
11f69daaec | ||
|
|
c0120c0f17 | ||
|
|
c1a5f9adf1 | ||
|
|
5087f27546 | ||
|
|
efbff9e217 | ||
|
|
20ea83ae93 | ||
|
|
05daeb561c | ||
|
|
bfff001752 | ||
|
|
c3a45a1584 | ||
|
|
b09a92a264 | ||
|
|
44a792583c | ||
|
|
71c8267dea | ||
|
|
b6688f56b5 | ||
|
|
f703164098 | ||
|
|
6a6b27e905 | ||
|
|
731a46c612 | ||
|
|
92a8078322 | ||
|
|
ba2d77f0bb | ||
|
|
3d21c15281 | ||
|
|
cb4b20c057 | ||
|
|
2af2767699 | ||
|
|
e4bb19b98a | ||
|
|
7e784c9509 | ||
|
|
3dd27797dc | ||
|
|
5e059272dc | ||
|
|
0a9aeca3bc | ||
|
|
11d42e0f93 | ||
|
|
85d8658037 | ||
|
|
dfa29950ef | ||
|
|
b7366a8704 | ||
|
|
57416103c3 | ||
|
|
72bd3731de | ||
|
|
9fab20ca6c | ||
|
|
8b4453f32d | ||
|
|
f4b77e6b03 | ||
|
|
c3da2fca9b | ||
|
|
c0d68c5740 | ||
|
|
5398564aec |
11
.travis.sh
11
.travis.sh
@@ -25,7 +25,7 @@ if [ "$1" == "doctests" ]; then
|
|||||||
cd doc
|
cd doc
|
||||||
make doctest
|
make doctest
|
||||||
fi
|
fi
|
||||||
if [ "$1" == "spelling" ]; then
|
if [ "$1" == "doc-spelling" ]; then
|
||||||
XDG_CACHE_HOME=/cache pip3 install -Ur doc/requirements.txt
|
XDG_CACHE_HOME=/cache pip3 install -Ur doc/requirements.txt
|
||||||
cd doc
|
cd doc
|
||||||
make spelling
|
make spelling
|
||||||
@@ -33,12 +33,17 @@ if [ "$1" == "spelling" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
if [ "$1" == "translation-spelling" ]; then
|
||||||
|
XDG_CACHE_HOME=/cache pip3 install -Ur src/requirements/dev.txt
|
||||||
|
cd src
|
||||||
|
potypo
|
||||||
|
fi
|
||||||
if [ "$1" == "tests" ]; then
|
if [ "$1" == "tests" ]; then
|
||||||
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt -r src/requirements/py34.txt
|
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt -r src/requirements/py34.txt pytest-xdist
|
||||||
cd src
|
cd src
|
||||||
python manage.py check
|
python manage.py check
|
||||||
make all compress
|
make all compress
|
||||||
py.test --reruns 5 tests
|
py.test --reruns 5 -n 2 tests
|
||||||
fi
|
fi
|
||||||
if [ "$1" == "tests-cov" ]; then
|
if [ "$1" == "tests-cov" ]; then
|
||||||
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt -r src/requirements/py34.txt
|
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt -r src/requirements/py34.txt
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ matrix:
|
|||||||
- python: 3.6
|
- python: 3.6
|
||||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
|
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
|
||||||
- python: 3.6
|
- python: 3.6
|
||||||
env: JOB=tests-cov
|
env: JOB=tests-cov PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||||
- python: 3.6
|
- python: 3.6
|
||||||
env: JOB=style
|
env: JOB=style
|
||||||
- python: 3.4
|
- python: 3.4
|
||||||
@@ -37,12 +37,16 @@ matrix:
|
|||||||
- python: 3.6
|
- python: 3.6
|
||||||
env: JOB=plugins
|
env: JOB=plugins
|
||||||
- python: 3.6
|
- python: 3.6
|
||||||
env: JOB=spelling
|
env: JOB=doc-spelling
|
||||||
|
- python: 3.6
|
||||||
|
env: JOB=translation-spelling
|
||||||
addons:
|
addons:
|
||||||
postgresql: "9.4"
|
postgresql: "9.4"
|
||||||
apt:
|
apt:
|
||||||
packages:
|
packages:
|
||||||
- enchant
|
- enchant
|
||||||
|
- myspell-de-de
|
||||||
|
- aspell-en
|
||||||
branches:
|
branches:
|
||||||
except:
|
except:
|
||||||
- /^weblate-.*/
|
- /^weblate-.*/
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ FROM python:3.6
|
|||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y git libxml2-dev libxslt1-dev python-dev python-virtualenv locales \
|
apt-get install -y git libxml2-dev libxslt1-dev python-dev python-virtualenv locales \
|
||||||
libffi-dev build-essential python3-dev zlib1g-dev libssl-dev gettext libpq-dev \
|
libffi-dev build-essential python3-dev zlib1g-dev libssl-dev gettext libpq-dev \
|
||||||
libmysqlclient-dev libmemcached-dev libjpeg-dev supervisor nginx sudo \
|
default-libmysqlclient-dev libmemcached-dev libjpeg-dev supervisor nginx sudo \
|
||||||
--no-install-recommends && \
|
--no-install-recommends && \
|
||||||
apt-get clean && \
|
apt-get clean && \
|
||||||
rm -rf /var/lib/apt/lists/* && \
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ cd /pretix/src
|
|||||||
export DJANGO_SETTINGS_MODULE=production_settings
|
export DJANGO_SETTINGS_MODULE=production_settings
|
||||||
export DATA_DIR=/data/
|
export DATA_DIR=/data/
|
||||||
export HOME=/pretix
|
export HOME=/pretix
|
||||||
NUM_WORKERS=10
|
export NUM_WORKERS=$((2 * $(nproc --all)))
|
||||||
|
|
||||||
if [ ! -d /data/logs ]; then
|
if [ ! -d /data/logs ]; then
|
||||||
mkdir /data/logs;
|
mkdir /data/logs;
|
||||||
|
|||||||
@@ -6,27 +6,13 @@ with pretix' REST API, such as authentication, pagination and similar definition
|
|||||||
|
|
||||||
.. _`rest-auth`:
|
.. _`rest-auth`:
|
||||||
|
|
||||||
Obtaining an API token
|
|
||||||
----------------------
|
|
||||||
|
|
||||||
To authenticate your API requests, you need to obtain an API token. You can create a
|
|
||||||
token in the pretix web interface on the level of organizer teams. Create a new team
|
|
||||||
or choose an existing team that has the level of permissions the token should have and
|
|
||||||
create a new token using the form below the list of team members:
|
|
||||||
|
|
||||||
.. image:: img/token_form.png
|
|
||||||
:class: screenshot
|
|
||||||
|
|
||||||
You can enter a description for the token to distinguish from other tokens later on.
|
|
||||||
Once you click "Add", you will be provided with an API token in the success message.
|
|
||||||
Copy this token, as you won't be able to retrieve it again.
|
|
||||||
|
|
||||||
.. image:: img/token_success.png
|
|
||||||
:class: screenshot
|
|
||||||
|
|
||||||
Authentication
|
Authentication
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
|
If you're building an application for end users, we strongly recommend that you use our
|
||||||
|
:ref:`OAuth-based authentication progress <rest-oauth>`. However, for simpler needs, you
|
||||||
|
can also go with static API tokens that you can create on a per-team basis (see below).
|
||||||
|
|
||||||
You need to include the API token with every request to pretix' API in the ``Authorization`` header
|
You need to include the API token with every request to pretix' API in the ``Authorization`` header
|
||||||
like the following:
|
like the following:
|
||||||
|
|
||||||
@@ -44,6 +30,24 @@ like the following:
|
|||||||
adding OAuth2 support in the future for user-level authentication. If you want
|
adding OAuth2 support in the future for user-level authentication. If you want
|
||||||
to use session authentication, be sure to comply with Django's `CSRF policies`_.
|
to use session authentication, be sure to comply with Django's `CSRF policies`_.
|
||||||
|
|
||||||
|
Obtaining an API token
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
To authenticate your API requests, you need to obtain an API token. You can create a
|
||||||
|
token in the pretix web interface on the level of organizer teams. Create a new team
|
||||||
|
or choose an existing team that has the level of permissions the token should have and
|
||||||
|
create a new token using the form below the list of team members:
|
||||||
|
|
||||||
|
.. image:: img/token_form.png
|
||||||
|
:class: screenshot
|
||||||
|
|
||||||
|
You can enter a description for the token to distinguish from other tokens later on.
|
||||||
|
Once you click "Add", you will be provided with an API token in the success message.
|
||||||
|
Copy this token, as you won't be able to retrieve it again.
|
||||||
|
|
||||||
|
.. image:: img/token_success.png
|
||||||
|
:class: screenshot
|
||||||
|
|
||||||
Permissions
|
Permissions
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
@@ -109,6 +113,41 @@ respective page.
|
|||||||
The field ``results`` contains a list of objects representing the first results. For most
|
The field ``results`` contains a list of objects representing the first results. For most
|
||||||
objects, every page contains 50 results.
|
objects, every page contains 50 results.
|
||||||
|
|
||||||
|
Conditional fetching
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
If you pull object lists from pretix' APIs regularly, we ask you to implement conditional fetching
|
||||||
|
to avoid unnecessary data traffic. This is not supported on all resources and we currently implement
|
||||||
|
two different mechanisms for different resources, which is necessary because we can only obtain best
|
||||||
|
efficiency for resources that do not support deletion operations.
|
||||||
|
|
||||||
|
Object-level conditional fetching
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
The :ref:`rest-orders` resource list contains an HTTP header called ``X-Page-Generated`` containing the
|
||||||
|
current time on the server in ISO 8601 format. On your next request, you can pass this header
|
||||||
|
(as is, without any modifications necessary) as the ``modified_since`` query parameter and you will receive
|
||||||
|
a list containing only objects that have changed in the time since your last request.
|
||||||
|
|
||||||
|
List-level conditional fetching
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
If modification checks are not possible with this granularity, you can instead check for the full list.
|
||||||
|
In this case, the list of objects may contain a regular HTTP header ``Last-Modified`` with the date of the
|
||||||
|
last modification to any item of that resource. You can then pass this date back in your next request in the
|
||||||
|
``If-Modified-Since`` header. If the any object has changed in the meantime, you will receive back a full list
|
||||||
|
(if something it missing, this means the object has been deleted). If nothing happened, we'll send back a
|
||||||
|
``304 Not Modified`` return code.
|
||||||
|
|
||||||
|
This is currently implemented on the following resources:
|
||||||
|
|
||||||
|
* :ref:`rest-categories`
|
||||||
|
* :ref:`rest-items`
|
||||||
|
* :ref:`rest-questions`
|
||||||
|
* :ref:`rest-quotas`
|
||||||
|
* :ref:`rest-subevents`
|
||||||
|
* :ref:`rest-taxrules`
|
||||||
|
|
||||||
Errors
|
Errors
|
||||||
------
|
------
|
||||||
|
|
||||||
|
|||||||
@@ -14,4 +14,5 @@ in functionality over time.
|
|||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
fundamentals
|
fundamentals
|
||||||
|
oauth
|
||||||
resources/index
|
resources/index
|
||||||
|
|||||||
171
doc/api/oauth.rst
Normal file
171
doc/api/oauth.rst
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
.. _`rest-oauth`:
|
||||||
|
|
||||||
|
OAuth support / "Connect with pretix"
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
In addition to static tokens, pretix supports `OAuth2`_-based authentication starting with
|
||||||
|
pretix 1.16. This allows you to put a "Connect with pretix" button into your website or tool
|
||||||
|
that allows the user to easily set up a connection between the two systems.
|
||||||
|
|
||||||
|
If you haven't worked with OAuth before, have a look at the `OAuth2 Simplified`_ tutorial.
|
||||||
|
|
||||||
|
Registering an application
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
To use OAuth, you need to register your application with the pretix instance you want to connect to.
|
||||||
|
In order to do this, log in to your pretix account and go to your user settings. Click on "Authorized applications"
|
||||||
|
first and then on "Manage your own apps". From there, you can "Create a new application".
|
||||||
|
|
||||||
|
You should fill in a descriptive name of your application that allows users to recognize who you are. You also need to
|
||||||
|
give a list of fully-qualified URLs that users will be redirected to after a successful authorization. After you pressed
|
||||||
|
"Save", you will be presented with a client ID and a client secret. Please note them down and treat the client secret
|
||||||
|
like a password; it should not become available to your users.
|
||||||
|
|
||||||
|
Obtaining an authorization grant
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
To authorize a new user, link or redirect them to the ``authorize`` endpoint, passing your client ID as a query
|
||||||
|
parameter. Additionally, you can pass a scope (currently either ``read``, ``write``, or ``read write``)
|
||||||
|
and an URL the user should be redirected to after successful or failed authorization. You also need to pass the
|
||||||
|
``response_type`` parameter with a value of ``code``. Example::
|
||||||
|
|
||||||
|
https://pretix.eu/api/v1/oauth/authorize?client_id=lsLi0hNL0vk53mEdYjNJxHUn1PcO1R6wVg81dLNT&response_type=code&scope=read+write&redirect_uri=https://pretalx.com
|
||||||
|
|
||||||
|
To prevent CSRF attacks, you can also optionally pass a ``state`` parameter with a random string. Later, when
|
||||||
|
redirecting back to your application, we will pass the same ``state`` parameter back to you, so you can compare if they
|
||||||
|
match.
|
||||||
|
|
||||||
|
After the user granted or denied access, they will be redirected back either to the ``redirect_url`` you passed in the
|
||||||
|
query or to the first redirect URL configured in your application settings.
|
||||||
|
|
||||||
|
On successful registration, we will append the query parameter ``code`` to the URL containing an authorization code.
|
||||||
|
For example, we might redirect the user to this URL::
|
||||||
|
|
||||||
|
https://pretalx.com/?code=eYBBf8gmeD4E01HLoj0XflqO4Lg3Cw&state=e3KCh9mfx07qxU4bRpXk
|
||||||
|
|
||||||
|
You will need this ``code`` parameter to perform the next step.
|
||||||
|
|
||||||
|
On a failed registration, a query string like ``?error=access_denied`` will be appended to the redirection URL.
|
||||||
|
|
||||||
|
.. note:: In this step, the user is allowed to restrict your access to certain organizer accounts. If you try to
|
||||||
|
re-authenticate the user later, the user might be instantly redirected back to you if authorization is already
|
||||||
|
given and would therefore be unable to review their organizer restriction settings. You can append the
|
||||||
|
``approval_prompt=force`` query parameter if you want to make sure the user actively needs to confirm the
|
||||||
|
authorization.
|
||||||
|
|
||||||
|
Getting an access token
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
Using the ``code`` value you obtained above and your client ID, you can now request an access token that actually gives
|
||||||
|
access to the API. The ``token`` endpoint expects you to authenticate using `HTTP Basic authentication`_ using your client
|
||||||
|
ID as a username and your client secret as a password. You are also required to again supply the same ``redirect_uri``
|
||||||
|
parameter that you used for the authorization.
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/oauth/token
|
||||||
|
|
||||||
|
Request a new access token
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/oauth/token HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Authorization: Basic bHNMaTBoTkwwdms1M21FZFlqTkp4SFVuMVBjTzFSNndWZzgxZExOVDplSmpzZVA0UjJMN0hMcjBiS0p1b3BmbnJtT2cyY3NDeTdYaFVVZ0FoalhUU0NhZHhRTjk3cVNvMkpPaXlWTFpQOEozaTVQd1FVdFIwNUNycG5ac2Z0bXJjdmNTbkZ1SkFmb2ZsUTdZUDRpSjZNTWFYTHIwQ0FpNlhIRFJjV1Awcg==
|
||||||
|
|
||||||
|
grant_type=authorization_code&code=eYBBf8gmeD4E01HLoj0XflqO4Lg3Cw&redirect_uri=https://pretalx.com
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"access_token": "i3ytqTSRWsKp16fqjekHXa4tdM4qNC",
|
||||||
|
"expires_in": 86400,
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"scope": "read write",
|
||||||
|
"refresh_token": "XBK0r8z4A4TTeR9LyMUyU2AM5rqpXp"
|
||||||
|
}
|
||||||
|
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
|
||||||
|
|
||||||
|
As you can see, you receive two types of tokens: One "access token", and one "refresh token". The access token is valid
|
||||||
|
for a day and can be used to actually access the API. The refresh token does not have an expiration date and can be used
|
||||||
|
to obtain a new access_token after a day, so you should make sure to store the access token safely if you need long-term
|
||||||
|
access.
|
||||||
|
|
||||||
|
Using the API with an access token
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
You can supply a valid access token as a ``Bearer``-type token in the ``Authorization`` header to get API access.
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
:emphasize-lines: 3
|
||||||
|
|
||||||
|
GET /api/v1/organizers/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Authorization: Bearer i3ytqTSRWsKp16fqjekHXa4tdM4qNC
|
||||||
|
|
||||||
|
Refreshing an access token
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
You can obtain a new access token using your refresh token any time. This can be done using the same ``token`` endpoint
|
||||||
|
used to obtain the first access token above, but with a different set of parameters:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/oauth/token HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Authorization: Basic bHNMaTBoTkwwdms1M21FZFlqTkp4SFVuMVBjTzFSNndWZzgxZExOVDplSmpzZVA0UjJMN0hMcjBiS0p1b3BmbnJtT2cyY3NDeTdYaFVVZ0FoalhUU0NhZHhRTjk3cVNvMkpPaXlWTFpQOEozaTVQd1FVdFIwNUNycG5ac2Z0bXJjdmNTbkZ1SkFmb2ZsUTdZUDRpSjZNTWFYTHIwQ0FpNlhIRFJjV1Awcg==
|
||||||
|
|
||||||
|
grant_type=refresh_token&refresh_token=XBK0r8z4A4TTeR9LyMUyU2AM5rqpXp
|
||||||
|
|
||||||
|
The previous access token will instantly become invalid.
|
||||||
|
|
||||||
|
Revoking a token
|
||||||
|
----------------
|
||||||
|
|
||||||
|
If you don't need a token any more or if you believe it may have been compromised, you can use the ``revoke_token``
|
||||||
|
endpoint to revoke it.
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/oauth/revoke_token
|
||||||
|
|
||||||
|
Revoke an access or refresh token. If you revoke an access token, you can still create a new one using the refresh token. If you
|
||||||
|
revoke a refresh token, the connected access token will also be revoked.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/oauth/revoke_token HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Authorization: Basic bHNMaTBoTkwwdms1M21FZFlqTkp4SFVuMVBjTzFSNndWZzgxZExOVDplSmpzZVA0UjJMN0hMcjBiS0p1b3BmbnJtT2cyY3NDeTdYaFVVZ0FoalhUU0NhZHhRTjk3cVNvMkpPaXlWTFpQOEozaTVQd1FVdFIwNUNycG5ac2Z0bXJjdmNTbkZ1SkFmb2ZsUTdZUDRpSjZNTWFYTHIwQ0FpNlhIRFJjV1Awcg==
|
||||||
|
|
||||||
|
token=XBK0r8z4A4TTeR9LyMUyU2AM5rqpXp
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
|
||||||
|
If you want to revoke your client secret, you can generate a new one in the list of your managed applications in the
|
||||||
|
pretix user interface.
|
||||||
|
|
||||||
|
.. _OAuth2: https://en.wikipedia.org/wiki/OAuth
|
||||||
|
.. _OAuth2 Simplified: https://aaronparecki.com/oauth-2-simplified/
|
||||||
|
.. _HTTP Basic authentication: https://en.wikipedia.org/wiki/Basic_access_authentication
|
||||||
258
doc/api/resources/carts.rst
Normal file
258
doc/api/resources/carts.rst
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
.. _rest-carts:
|
||||||
|
|
||||||
|
Cart positions
|
||||||
|
==============
|
||||||
|
|
||||||
|
The API provides limited access to the cart position data model. This API currently only allows creating and deleting
|
||||||
|
cart positions to reserve quota.
|
||||||
|
|
||||||
|
Cart position resource
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
The cart position resource contains the following public fields:
|
||||||
|
|
||||||
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
Field Type Description
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
id integer Internal ID of the cart position
|
||||||
|
cart_id string Identifier of the cart this belongs to. Needs to end
|
||||||
|
in "@api" for API-created positions.
|
||||||
|
datetime datetime Time of creation
|
||||||
|
expires datetime The cart position will expire at this time and no longer block quota
|
||||||
|
item integer ID of the item
|
||||||
|
variation integer ID of the variation (or ``null``)
|
||||||
|
price money (string) Price of this position
|
||||||
|
attendee_name string Specified attendee name for this position (or ``null``)
|
||||||
|
attendee_email string Specified attendee email address for this position (or ``null``)
|
||||||
|
voucher integer Internal ID of the voucher used for this position (or ``null``)
|
||||||
|
addon_to integer Internal ID of the position this position is an add-on for (or ``null``)
|
||||||
|
subevent integer ID of the date inside an event series this position belongs to (or ``null``).
|
||||||
|
answers list of objects Answers to user-defined questions
|
||||||
|
├ question integer Internal ID of the answered question
|
||||||
|
├ answer string Text representation of the answer
|
||||||
|
├ question_identifier string The question's ``identifier`` field
|
||||||
|
├ options list of integers Internal IDs of selected option(s)s (only for choice types)
|
||||||
|
└ option_identifiers list of strings The ``identifier`` fields of the selected option(s)s
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
.. versionchanged:: 1.17
|
||||||
|
|
||||||
|
This resource has been added.
|
||||||
|
|
||||||
|
|
||||||
|
Cart position endpoints
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/cartpositions/
|
||||||
|
|
||||||
|
Returns a list of API-created cart positions.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/cartpositions/ 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": 1,
|
||||||
|
"cart_id": "XwokV8FojQviD9jhtDzKvHFdlLRNMhlfo3cNjGbuK6MUTQDT@api",
|
||||||
|
"item": 1,
|
||||||
|
"variation": null,
|
||||||
|
"price": "23.00",
|
||||||
|
"attendee_name": null,
|
||||||
|
"attendee_email": null,
|
||||||
|
"voucher": null,
|
||||||
|
"addon_to": null,
|
||||||
|
"subevent": null,
|
||||||
|
"datetime": "2018-06-11T10:00:00Z",
|
||||||
|
"expires": "2018-06-11T10:00:00Z",
|
||||||
|
"includes_tax": true,
|
||||||
|
"answers": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||||
|
: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)/events/(event)/cartpositions/(id)/
|
||||||
|
|
||||||
|
Returns information on one cart position, identified by its internal ID.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/cartpositions/1/ 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
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"cart_id": "XwokV8FojQviD9jhtDzKvHFdlLRNMhlfo3cNjGbuK6MUTQDT@api",
|
||||||
|
"item": 1,
|
||||||
|
"variation": null,
|
||||||
|
"price": "23.00",
|
||||||
|
"attendee_name": null,
|
||||||
|
"attendee_email": null,
|
||||||
|
"voucher": null,
|
||||||
|
"addon_to": null,
|
||||||
|
"subevent": null,
|
||||||
|
"datetime": "2018-06-11T10:00:00Z",
|
||||||
|
"expires": "2018-06-11T10:00:00Z",
|
||||||
|
"includes_tax": true,
|
||||||
|
"answers": []
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param event: The ``slug`` field of the event to fetch
|
||||||
|
:param id: The ``id`` field of the position 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.
|
||||||
|
:statuscode 404: The requested cart position does not exist.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/cartpositions/
|
||||||
|
|
||||||
|
Creates a new cart position.
|
||||||
|
|
||||||
|
.. warning:: This endpoint is considered **experimental**. It might change at any time without prior notice.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
This endpoint is intended for advanced users. It is not designed to be used to build your own shop frontend.
|
||||||
|
There is a lot that it does not or can not do, and you will need to be careful using it.
|
||||||
|
It allows to bypass many of the restrictions imposed when creating a cart through the
|
||||||
|
regular shop.
|
||||||
|
|
||||||
|
Specifically, this endpoint currently
|
||||||
|
|
||||||
|
* does not validate if products are only to be sold in a specific time frame
|
||||||
|
|
||||||
|
* does not validate if the event's ticket sales are already over or haven't started
|
||||||
|
|
||||||
|
* does not support add-on products at the moment
|
||||||
|
|
||||||
|
* does not check or calculate prices but believes any prices you send
|
||||||
|
|
||||||
|
* does not support the redemption of vouchers
|
||||||
|
|
||||||
|
* does not prevent you from buying items that can only be bought with a voucher
|
||||||
|
|
||||||
|
* does not support file upload questions
|
||||||
|
|
||||||
|
You can supply the following fields of the resource:
|
||||||
|
|
||||||
|
* ``cart_id`` (optional, needs to end in ``@api``)
|
||||||
|
* ``item``
|
||||||
|
* ``variation`` (optional)
|
||||||
|
* ``price``
|
||||||
|
* ``attendee_name`` (optional)
|
||||||
|
* ``attendee_email`` (optional)
|
||||||
|
* ``subevent`` (optional)
|
||||||
|
* ``expires`` (optional)
|
||||||
|
* ``includes_tax`` (optional)
|
||||||
|
* ``answers``
|
||||||
|
|
||||||
|
* ``question``
|
||||||
|
* ``answer``
|
||||||
|
* ``options``
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/cartpositions/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"item": 1,
|
||||||
|
"variation": null,
|
||||||
|
"price": "23.00",
|
||||||
|
"attendee_name": "Peter",
|
||||||
|
"attendee_email": null,
|
||||||
|
"answers": [
|
||||||
|
{
|
||||||
|
"question": 1,
|
||||||
|
"answer": "23",
|
||||||
|
"options": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subevent": null
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
(Full cart position resource, see above.)
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer of the event to create a position for
|
||||||
|
:param event: The ``slug`` field of the event to create a position for
|
||||||
|
:statuscode 201: no error
|
||||||
|
:statuscode 400: The item could not be created due to invalid submitted data or lack of quota.
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this
|
||||||
|
order.
|
||||||
|
|
||||||
|
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/cartpositions/(id)/
|
||||||
|
|
||||||
|
Deletes a cart position, identified by its internal ID.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
DELETE /api/v1/organizers/bigevents/events/sampleconf/cartpositions/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 204 No Content
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param event: The ``slug`` field of the event to fetch
|
||||||
|
:param id: The ``id`` field of the position to delete
|
||||||
|
: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.
|
||||||
|
:statuscode 404: The requested cart position does not exist.
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
.. _`rest-categories`:
|
||||||
|
|
||||||
Item categories
|
Item categories
|
||||||
===============
|
===============
|
||||||
|
|
||||||
@@ -14,6 +16,7 @@ Field Type Description
|
|||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
id integer Internal ID of the category
|
id integer Internal ID of the category
|
||||||
name multi-lingual string The category's visible name
|
name multi-lingual string The category's visible name
|
||||||
|
internal_name string An optional name that is only used in the backend
|
||||||
description multi-lingual string A public description (might include markdown, can
|
description multi-lingual string A public description (might include markdown, can
|
||||||
be ``null``)
|
be ``null``)
|
||||||
position integer An integer, used for sorting the categories
|
position integer An integer, used for sorting the categories
|
||||||
@@ -26,6 +29,10 @@ is_addon boolean If ``True``, it
|
|||||||
|
|
||||||
The operations POST, PATCH, PUT and DELETE have been added.
|
The operations POST, PATCH, PUT and DELETE have been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.16
|
||||||
|
|
||||||
|
The field ``internal_name`` has been added.
|
||||||
|
|
||||||
|
|
||||||
Endpoints
|
Endpoints
|
||||||
---------
|
---------
|
||||||
@@ -58,6 +65,7 @@ Endpoints
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "Tickets"},
|
"name": {"en": "Tickets"},
|
||||||
|
"internal_name": "",
|
||||||
"description": {"en": "Tickets are what you need to get in."},
|
"description": {"en": "Tickets are what you need to get in."},
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"is_addon": false
|
"is_addon": false
|
||||||
@@ -99,6 +107,7 @@ Endpoints
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "Tickets"},
|
"name": {"en": "Tickets"},
|
||||||
|
"internal_name": "",
|
||||||
"description": {"en": "Tickets are what you need to get in."},
|
"description": {"en": "Tickets are what you need to get in."},
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"is_addon": false
|
"is_addon": false
|
||||||
@@ -126,6 +135,7 @@ Endpoints
|
|||||||
|
|
||||||
{
|
{
|
||||||
"name": {"en": "Tickets"},
|
"name": {"en": "Tickets"},
|
||||||
|
"internal_name": "",
|
||||||
"description": {"en": "Tickets are what you need to get in."},
|
"description": {"en": "Tickets are what you need to get in."},
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"is_addon": false
|
"is_addon": false
|
||||||
@@ -142,6 +152,7 @@ Endpoints
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "Tickets"},
|
"name": {"en": "Tickets"},
|
||||||
|
"internal_name": "",
|
||||||
"description": {"en": "Tickets are what you need to get in."},
|
"description": {"en": "Tickets are what you need to get in."},
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"is_addon": false
|
"is_addon": false
|
||||||
@@ -187,6 +198,7 @@ Endpoints
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "Tickets"},
|
"name": {"en": "Tickets"},
|
||||||
|
"internal_name": "",
|
||||||
"description": {"en": "Tickets are what you need to get in."},
|
"description": {"en": "Tickets are what you need to get in."},
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"is_addon": true
|
"is_addon": true
|
||||||
|
|||||||
@@ -375,6 +375,7 @@ Order position endpoints
|
|||||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||||
"addon_to": null,
|
"addon_to": null,
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
|
"pseudonymization_id": "MQLJvANO3B",
|
||||||
"checkins": [
|
"checkins": [
|
||||||
{
|
{
|
||||||
"list": 1,
|
"list": 1,
|
||||||
@@ -467,6 +468,7 @@ Order position endpoints
|
|||||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||||
"addon_to": null,
|
"addon_to": null,
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
|
"pseudonymization_id": "MQLJvANO3B",
|
||||||
"checkins": [
|
"checkins": [
|
||||||
{
|
{
|
||||||
"list": 1,
|
"list": 1,
|
||||||
|
|||||||
@@ -20,3 +20,4 @@ Resources and endpoints
|
|||||||
vouchers
|
vouchers
|
||||||
checkinlists
|
checkinlists
|
||||||
waitinglist
|
waitinglist
|
||||||
|
carts
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
.. _rest-items:
|
||||||
|
|
||||||
Items
|
Items
|
||||||
=====
|
=====
|
||||||
|
|
||||||
@@ -14,6 +16,7 @@ Field Type Description
|
|||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
id integer Internal ID of the item
|
id integer Internal ID of the item
|
||||||
name multi-lingual string The item's visible name
|
name multi-lingual string The item's visible name
|
||||||
|
internal_name string An optional name that is only used in the backend
|
||||||
default_price money (string) The item price that is applied if the price is not
|
default_price money (string) The item price that is applied if the price is not
|
||||||
overwritten by variations or other options.
|
overwritten by variations or other options.
|
||||||
category integer The ID of the category this item belongs to
|
category integer The ID of the category this item belongs to
|
||||||
@@ -54,11 +57,14 @@ max_per_order integer This product ca
|
|||||||
checkin_attention boolean If ``True``, the check-in app should show a warning
|
checkin_attention boolean If ``True``, the check-in app should show a warning
|
||||||
that this ticket requires special attention if such
|
that this ticket requires special attention if such
|
||||||
a product is being scanned.
|
a product is being scanned.
|
||||||
|
original_price money (string) An original price, shown for comparison, not used
|
||||||
|
for price calculations.
|
||||||
has_variations boolean Shows whether or not this item has variations.
|
has_variations boolean Shows whether or not this item has variations.
|
||||||
variations list of objects A list with one object for each variation of this item.
|
variations list of objects A list with one object for each variation of this item.
|
||||||
Can be empty. Only writable during creation,
|
Can be empty. Only writable during creation,
|
||||||
use separate endpoint to modify this later.
|
use separate endpoint to modify this later.
|
||||||
├ id integer Internal ID of the variation
|
├ id integer Internal ID of the variation
|
||||||
|
├ value multi-lingual string The "name" of the variation
|
||||||
├ default_price money (string) The price set directly for this variation or ``null``
|
├ default_price money (string) The price set directly for this variation or ``null``
|
||||||
├ price money (string) The price used for this variation. This is either the
|
├ price money (string) The price used for this variation. This is either the
|
||||||
same as ``default_price`` if that value is set or equal
|
same as ``default_price`` if that value is set or equal
|
||||||
@@ -88,6 +94,10 @@ addons list of objects Definition of a
|
|||||||
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
|
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
|
||||||
The attribute ``price_included`` has been added to ``addons``.
|
The attribute ``price_included`` has been added to ``addons``.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.16
|
||||||
|
|
||||||
|
The field ``internal_name`` and ``original_price`` fields have been added.
|
||||||
|
|
||||||
Notes
|
Notes
|
||||||
-----
|
-----
|
||||||
Please note that an item either always has variations or never has. Once created with variations the item can never
|
Please note that an item either always has variations or never has. Once created with variations the item can never
|
||||||
@@ -129,7 +139,9 @@ Endpoints
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "Standard ticket"},
|
"name": {"en": "Standard ticket"},
|
||||||
|
"internal_name": "",
|
||||||
"default_price": "23.00",
|
"default_price": "23.00",
|
||||||
|
"original_price": null,
|
||||||
"category": null,
|
"category": null,
|
||||||
"active": true,
|
"active": true,
|
||||||
"description": null,
|
"description": null,
|
||||||
@@ -211,7 +223,9 @@ Endpoints
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "Standard ticket"},
|
"name": {"en": "Standard ticket"},
|
||||||
|
"internal_name": "",
|
||||||
"default_price": "23.00",
|
"default_price": "23.00",
|
||||||
|
"original_price": null,
|
||||||
"category": null,
|
"category": null,
|
||||||
"active": true,
|
"active": true,
|
||||||
"description": null,
|
"description": null,
|
||||||
@@ -274,7 +288,9 @@ Endpoints
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "Standard ticket"},
|
"name": {"en": "Standard ticket"},
|
||||||
|
"internal_name": "",
|
||||||
"default_price": "23.00",
|
"default_price": "23.00",
|
||||||
|
"original_price": null,
|
||||||
"category": null,
|
"category": null,
|
||||||
"active": true,
|
"active": true,
|
||||||
"description": null,
|
"description": null,
|
||||||
@@ -324,7 +340,9 @@ Endpoints
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "Standard ticket"},
|
"name": {"en": "Standard ticket"},
|
||||||
|
"internal_name": "",
|
||||||
"default_price": "23.00",
|
"default_price": "23.00",
|
||||||
|
"original_price": null,
|
||||||
"category": null,
|
"category": null,
|
||||||
"active": true,
|
"active": true,
|
||||||
"description": null,
|
"description": null,
|
||||||
@@ -406,7 +424,9 @@ Endpoints
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "Ticket"},
|
"name": {"en": "Ticket"},
|
||||||
|
"internal_name": "",
|
||||||
"default_price": "25.00",
|
"default_price": "25.00",
|
||||||
|
"original_price": null,
|
||||||
"category": null,
|
"category": null,
|
||||||
"active": true,
|
"active": true,
|
||||||
"description": null,
|
"description": null,
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
.. spelling:: checkins
|
.. spelling::
|
||||||
|
|
||||||
|
checkins
|
||||||
|
pdf
|
||||||
|
|
||||||
|
|
||||||
|
.. _rest-orders:
|
||||||
|
|
||||||
Orders
|
Orders
|
||||||
======
|
======
|
||||||
@@ -49,7 +55,7 @@ invoice_address object Invoice address
|
|||||||
└ vat_id_validated string ``True``, if the VAT ID has been validated against the
|
└ vat_id_validated string ``True``, if the VAT ID has been validated against the
|
||||||
EU VAT service and validation was successful. This only
|
EU VAT service and validation was successful. This only
|
||||||
happens in rare cases.
|
happens in rare cases.
|
||||||
position list of objects List of order positions (see below)
|
positions list of objects List of order positions (see below)
|
||||||
fees list of objects List of fees included in the order total (i.e.
|
fees list of objects List of fees included in the order total (i.e.
|
||||||
payment fees)
|
payment fees)
|
||||||
├ fee_type string Type of fee (currently ``payment``, ``passbook``,
|
├ fee_type string Type of fee (currently ``payment``, ``passbook``,
|
||||||
@@ -68,6 +74,7 @@ downloads list of objects List of ticket
|
|||||||
download options.
|
download options.
|
||||||
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
|
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
|
||||||
└ url string Download URL
|
└ url string Download URL
|
||||||
|
last_modified datetime Last modification of this object
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|
||||||
@@ -96,6 +103,11 @@ downloads list of objects List of ticket
|
|||||||
The attributes ``order.payment_fee``, ``order.payment_fee_tax_rate``, ``order.payment_fee_tax_value`` and
|
The attributes ``order.payment_fee``, ``order.payment_fee_tax_rate``, ``order.payment_fee_tax_value`` and
|
||||||
``order.payment_fee_tax_rule`` have finally been removed.
|
``order.payment_fee_tax_rule`` have finally been removed.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.16
|
||||||
|
|
||||||
|
The attributes ``order.last_modified`` as well as the corresponding filters to the resource have been added.
|
||||||
|
An endpoint for order creation as well as ``…/mark_refunded/`` has been added.
|
||||||
|
|
||||||
.. _order-position-resource:
|
.. _order-position-resource:
|
||||||
|
|
||||||
Order position resource
|
Order position resource
|
||||||
@@ -107,7 +119,7 @@ Order position resource
|
|||||||
Field Type Description
|
Field Type Description
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
id integer Internal ID of the order position
|
id integer Internal ID of the order position
|
||||||
code string Order code of the order the position belongs to
|
order string Order code of the order the position belongs to
|
||||||
positionid integer Number of the position within the order
|
positionid integer Number of the position within the order
|
||||||
item integer ID of the purchased item
|
item integer ID of the purchased item
|
||||||
variation integer ID of the purchased variation (or ``null``)
|
variation integer ID of the purchased variation (or ``null``)
|
||||||
@@ -121,6 +133,7 @@ tax_rule integer The ID of the u
|
|||||||
secret string Secret code printed on the tickets for validation
|
secret string Secret code printed on the tickets for validation
|
||||||
addon_to integer Internal ID of the position this position is an add-on for (or ``null``)
|
addon_to integer Internal ID of the position this position is an add-on for (or ``null``)
|
||||||
subevent integer ID of the date inside an event series this position belongs to (or ``null``).
|
subevent integer ID of the date inside an event series this position belongs to (or ``null``).
|
||||||
|
pseudonymization_id string A random ID, e.g. for use in lead scanning apps
|
||||||
checkins list of objects List of check-ins with this ticket
|
checkins list of objects List of check-ins with this ticket
|
||||||
├ list integer Internal ID of the check-in list
|
├ list integer Internal ID of the check-in list
|
||||||
└ datetime datetime Time of check-in
|
└ datetime datetime Time of check-in
|
||||||
@@ -133,6 +146,9 @@ answers list of objects Answers to user
|
|||||||
├ question_identifier string The question's ``identifier`` field
|
├ question_identifier string The question's ``identifier`` field
|
||||||
├ options list of integers Internal IDs of selected option(s)s (only for choice types)
|
├ options list of integers Internal IDs of selected option(s)s (only for choice types)
|
||||||
└ option_identifiers list of strings The ``identifier`` fields of the selected option(s)s
|
└ option_identifiers list of strings The ``identifier`` fields of the selected option(s)s
|
||||||
|
pdf_data object Data object required for ticket PDF generation. By default,
|
||||||
|
this field is missing. It will be added only if you add the
|
||||||
|
``pdf_data=true`` query parameter to your request.
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
.. versionchanged:: 1.7
|
.. versionchanged:: 1.7
|
||||||
@@ -147,6 +163,10 @@ answers list of objects Answers to user
|
|||||||
|
|
||||||
The attributes ``answers.question_identifier`` and ``answers.option_identifiers`` have been added.
|
The attributes ``answers.question_identifier`` and ``answers.option_identifiers`` have been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.16
|
||||||
|
|
||||||
|
The attributes ``pseudonymization_id`` and ``pdf_data`` have been added.
|
||||||
|
|
||||||
|
|
||||||
Order endpoints
|
Order endpoints
|
||||||
---------------
|
---------------
|
||||||
@@ -174,6 +194,7 @@ Order endpoints
|
|||||||
HTTP/1.1 200 OK
|
HTTP/1.1 200 OK
|
||||||
Vary: Accept
|
Vary: Accept
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
X-Page-Generated: 2017-12-01T10:00:00Z
|
||||||
|
|
||||||
{
|
{
|
||||||
"count": 1,
|
"count": 1,
|
||||||
@@ -188,6 +209,7 @@ Order endpoints
|
|||||||
"locale": "en",
|
"locale": "en",
|
||||||
"datetime": "2017-12-01T10:00:00Z",
|
"datetime": "2017-12-01T10:00:00Z",
|
||||||
"expires": "2017-12-10T10:00:00Z",
|
"expires": "2017-12-10T10:00:00Z",
|
||||||
|
"last_modified": "2017-12-01T10:00:00Z",
|
||||||
"payment_date": "2017-12-05",
|
"payment_date": "2017-12-05",
|
||||||
"payment_provider": "banktransfer",
|
"payment_provider": "banktransfer",
|
||||||
"fees": [],
|
"fees": [],
|
||||||
@@ -224,6 +246,7 @@ Order endpoints
|
|||||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||||
"addon_to": null,
|
"addon_to": null,
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
|
"pseudonymization_id": "MQLJvANO3B",
|
||||||
"checkins": [
|
"checkins": [
|
||||||
{
|
{
|
||||||
"list": 44,
|
"list": 44,
|
||||||
@@ -264,8 +287,11 @@ Order endpoints
|
|||||||
:query string status: Only return orders in the given order status (see above)
|
:query string status: Only return orders in the given order status (see above)
|
||||||
:query string email: Only return orders created with the given email address
|
:query string email: Only return orders created with the given email address
|
||||||
:query string locale: Only return orders with the given customer locale
|
:query string locale: Only return orders with the given customer locale
|
||||||
|
:query datetime modified_since: Only return orders that have changed since the given date
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
:param event: The ``slug`` field of the event to fetch
|
:param event: The ``slug`` field of the event to fetch
|
||||||
|
: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 ``modified_since`` in your next call.
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||||
@@ -298,6 +324,7 @@ Order endpoints
|
|||||||
"locale": "en",
|
"locale": "en",
|
||||||
"datetime": "2017-12-01T10:00:00Z",
|
"datetime": "2017-12-01T10:00:00Z",
|
||||||
"expires": "2017-12-10T10:00:00Z",
|
"expires": "2017-12-10T10:00:00Z",
|
||||||
|
"last_modified": "2017-12-01T10:00:00Z",
|
||||||
"payment_date": "2017-12-05",
|
"payment_date": "2017-12-05",
|
||||||
"payment_provider": "banktransfer",
|
"payment_provider": "banktransfer",
|
||||||
"fees": [],
|
"fees": [],
|
||||||
@@ -334,6 +361,7 @@ Order endpoints
|
|||||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||||
"addon_to": null,
|
"addon_to": null,
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
|
"pseudonymization_id": "MQLJvANO3B",
|
||||||
"checkins": [
|
"checkins": [
|
||||||
{
|
{
|
||||||
"list": 44,
|
"list": 44,
|
||||||
@@ -414,6 +442,182 @@ Order endpoints
|
|||||||
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting for a few
|
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting for a few
|
||||||
seconds.
|
seconds.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/
|
||||||
|
|
||||||
|
Creates a new order.
|
||||||
|
|
||||||
|
.. warning:: This endpoint is considered **experimental**. It might change at any time without prior notice.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
This endpoint is intended for advanced users. It is not designed to be used to build your own shop frontend,
|
||||||
|
it's rather intended to import attendees from external sources etc.
|
||||||
|
There is a lot that it does not or can not do, and you will need to be careful using it.
|
||||||
|
It allows to bypass many of the restrictions imposed when creating an order through the
|
||||||
|
regular shop.
|
||||||
|
|
||||||
|
Specifically, this endpoint currently
|
||||||
|
|
||||||
|
* does not validate if products are only to be sold in a specific time frame
|
||||||
|
|
||||||
|
* does not validate if the event's ticket sales are already over or haven't started
|
||||||
|
|
||||||
|
* does not validate the number of items per order or the number of times an item can be included in an order
|
||||||
|
|
||||||
|
* does not validate any requirements related to add-on products
|
||||||
|
|
||||||
|
* does not check or calculate prices but believes any prices you send
|
||||||
|
|
||||||
|
* does not support the redemption of vouchers
|
||||||
|
|
||||||
|
* does not prevent you from buying items that can only be bought with a voucher
|
||||||
|
|
||||||
|
* does not calculate fees
|
||||||
|
|
||||||
|
* does not allow to pass data to plugins and will therefore cause issues with some plugins like the shipping
|
||||||
|
module
|
||||||
|
|
||||||
|
* does not send order confirmations via email
|
||||||
|
|
||||||
|
* does not support reverse charge taxation
|
||||||
|
|
||||||
|
* does not support file upload questions
|
||||||
|
|
||||||
|
You can supply the following fields of the resource:
|
||||||
|
|
||||||
|
* ``code`` (optional)
|
||||||
|
* ``status`` (optional) – Defaults to pending for non-free orders and paid for free orders. You can only set this to
|
||||||
|
``"n"`` for pending or ``"p"`` for paid. If you create a paid order, the ``order_paid`` signal will **not** be
|
||||||
|
sent out to plugins and no email will be sent. If you want that behavior, create an unpaid order and then call
|
||||||
|
the ``mark_paid`` API method.
|
||||||
|
* ``consume_carts`` (optional) – A list of cart IDs. All cart positions with these IDs will be deleted if the
|
||||||
|
order creation is successful. Any quotas that become free by this operation will be credited to your order
|
||||||
|
creation.
|
||||||
|
* ``email``
|
||||||
|
* ``locale``
|
||||||
|
* ``payment_provider`` – The identifier of the payment provider set for this order. This needs to be an existing
|
||||||
|
payment provider. You should use ``"free"`` for free orders.
|
||||||
|
* ``payment_info`` (optional) – You can pass a nested JSON object that will be set as the internal ``payment_info``
|
||||||
|
value of the order. How this value is handled is up to the payment provider and you should only use this if you
|
||||||
|
know the specific payment provider in detail. Please keep in mind that the payment provider will not be called
|
||||||
|
to do anything about this (i.e. if you pass a bank account to a debit provider, *no* charge will be created),
|
||||||
|
this is just informative in case you *handled the payment already*.
|
||||||
|
* ``comment`` (optional)
|
||||||
|
* ``checkin_attention`` (optional)
|
||||||
|
* ``invoice_address`` (optional)
|
||||||
|
|
||||||
|
* ``company``
|
||||||
|
* ``is_business``
|
||||||
|
* ``name``
|
||||||
|
* ``street``
|
||||||
|
* ``zipcode``
|
||||||
|
* ``city``
|
||||||
|
* ``country``
|
||||||
|
* ``internal_reference``
|
||||||
|
* ``vat_id``
|
||||||
|
|
||||||
|
* ``positions``
|
||||||
|
|
||||||
|
* ``positionid`` (optional, see below)
|
||||||
|
* ``item``
|
||||||
|
* ``variation``
|
||||||
|
* ``price``
|
||||||
|
* ``attendee_name``
|
||||||
|
* ``attendee_email``
|
||||||
|
* ``secret`` (optional)
|
||||||
|
* ``addon_to`` (optional, see below)
|
||||||
|
* ``subevent``
|
||||||
|
* ``answers``
|
||||||
|
|
||||||
|
* ``question``
|
||||||
|
* ``answer``
|
||||||
|
* ``options``
|
||||||
|
|
||||||
|
* ``fees``
|
||||||
|
|
||||||
|
* ``fee_type``
|
||||||
|
* ``value``
|
||||||
|
* ``description``
|
||||||
|
* ``internal_type``
|
||||||
|
* ``tax_rule``
|
||||||
|
|
||||||
|
If you want to use add-on products, you need to set the ``positionid`` fields of all positions manually
|
||||||
|
to incrementing integers starting with ``1``. Then, you can reference one of these
|
||||||
|
IDs in the ``addon_to`` field of another position. Note that all add_ons for a specific position need to come
|
||||||
|
immediately after the position itself.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "dummy@example.org",
|
||||||
|
"locale": "en",
|
||||||
|
"fees": [
|
||||||
|
{
|
||||||
|
"fee_type": "payment",
|
||||||
|
"value": "0.25",
|
||||||
|
"description": "",
|
||||||
|
"internal_type": "",
|
||||||
|
"tax_rule": 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payment_provider": "banktransfer",
|
||||||
|
"invoice_address": {
|
||||||
|
"is_business": False,
|
||||||
|
"company": "Sample company",
|
||||||
|
"name": "John Doe",
|
||||||
|
"street": "Sesam Street 12",
|
||||||
|
"zipcode": "12345",
|
||||||
|
"city": "Sample City",
|
||||||
|
"country": "UK",
|
||||||
|
"internal_reference": "",
|
||||||
|
"vat_id": ""
|
||||||
|
},
|
||||||
|
"positions": [
|
||||||
|
{
|
||||||
|
"positionid": 1,
|
||||||
|
"item": 1,
|
||||||
|
"variation": null,
|
||||||
|
"price": "23.00",
|
||||||
|
"attendee_name": "Peter",
|
||||||
|
"attendee_email": null,
|
||||||
|
"addon_to": null,
|
||||||
|
"answers": [
|
||||||
|
{
|
||||||
|
"question": 1,
|
||||||
|
"answer": "23",
|
||||||
|
"options": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subevent": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
(Full order resource, see above.)
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer of the event to create an item for
|
||||||
|
:param event: The ``slug`` field of the event to create an item for
|
||||||
|
:statuscode 201: no error
|
||||||
|
:statuscode 400: The item could not be created due to invalid submitted data or lack of quota.
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this
|
||||||
|
order.
|
||||||
|
|
||||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/mark_paid/
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/mark_paid/
|
||||||
|
|
||||||
Marks a pending or expired order as successfully paid.
|
Marks a pending or expired order as successfully paid.
|
||||||
@@ -525,6 +729,44 @@ Order endpoints
|
|||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||||
:statuscode 404: The requested order does not exist.
|
:statuscode 404: The requested order does not exist.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/mark_refunded/
|
||||||
|
|
||||||
|
Marks a paid order as refunded.
|
||||||
|
|
||||||
|
.. warning:: In the current implementation, this will **bypass** the payment provider, i.e. the money will **not** be
|
||||||
|
transferred back to the user automatically, the order will only be *marked* as refunded within pretix.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/mark_expired/ 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
|
||||||
|
|
||||||
|
{
|
||||||
|
"code": "ABC12",
|
||||||
|
"status": "r",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param event: The ``slug`` field of the event to modify
|
||||||
|
:param code: The ``code`` field of the order to modify
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 400: The order cannot be marked as expired since the current order status does not allow it.
|
||||||
|
: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 does not exist.
|
||||||
|
|
||||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/mark_expired/
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/mark_expired/
|
||||||
|
|
||||||
Marks a unpaid order as expired.
|
Marks a unpaid order as expired.
|
||||||
@@ -659,6 +901,7 @@ Order position endpoints
|
|||||||
"tax_rule": null,
|
"tax_rule": null,
|
||||||
"tax_value": "0.00",
|
"tax_value": "0.00",
|
||||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||||
|
"pseudonymization_id": "MQLJvANO3B",
|
||||||
"addon_to": null,
|
"addon_to": null,
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
"checkins": [
|
"checkins": [
|
||||||
@@ -751,6 +994,7 @@ Order position endpoints
|
|||||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||||
"addon_to": null,
|
"addon_to": null,
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
|
"pseudonymization_id": "MQLJvANO3B",
|
||||||
"checkins": [
|
"checkins": [
|
||||||
{
|
{
|
||||||
"list": 44,
|
"list": 44,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
.. spelling:: checkin
|
.. spelling:: checkin
|
||||||
|
|
||||||
|
.. _rest-questions:
|
||||||
|
|
||||||
Questions
|
Questions
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
.. _rest-quotas:
|
||||||
|
|
||||||
Quotas
|
Quotas
|
||||||
======
|
======
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
.. _rest-subevents:
|
||||||
|
|
||||||
Event series dates / Sub-events
|
Event series dates / Sub-events
|
||||||
===============================
|
===============================
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
.. _rest-taxrules:
|
||||||
|
|
||||||
Tax rules
|
Tax rules
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ Backend
|
|||||||
-------
|
-------
|
||||||
|
|
||||||
.. automodule:: pretix.control.signals
|
.. automodule:: pretix.control.signals
|
||||||
:members: nav_event, html_head, quota_detail_html, nav_topbar, nav_global, nav_organizer, nav_event_settings, order_info, event_settings_widget
|
:members: nav_event, html_head, quota_detail_html, nav_topbar, nav_global, nav_organizer, nav_event_settings, order_info, event_settings_widget, oauth_application_registered
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
|
|||||||
@@ -122,13 +122,15 @@ for example, to check for any errors in any staged files when committing::
|
|||||||
export GIT_WORK_TREE=../
|
export GIT_WORK_TREE=../
|
||||||
export GIT_DIR=../.git
|
export GIT_DIR=../.git
|
||||||
source ../env/bin/activate # Adjust to however you activate your virtual environment
|
source ../env/bin/activate # Adjust to however you activate your virtual environment
|
||||||
for file in $(git diff --cached --name-only | grep -E '\.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
|
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
|
git show ":$file" | flake8 - --stdin-display-name="$file" || exit 1 # we only want to lint the staged changes, not any un-staged changes
|
||||||
git show ":$file" | isort -df --check-only - | grep ERROR && exit 1 || true
|
git show ":$file" | isort -df --check-only - | grep ERROR && exit 1 || true
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
This keeps you from accidentally creating commits violating the style guide.
|
This keeps you from accidentally creating commits violating the style guide.
|
||||||
|
|
||||||
Working with mails
|
Working with mails
|
||||||
|
|||||||
110
doc/plugins/badges.rst
Normal file
110
doc/plugins/badges.rst
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
Badges
|
||||||
|
======
|
||||||
|
|
||||||
|
The badges plugin provides a HTTP API that exposes the various layouts used to generate PDF badges.
|
||||||
|
|
||||||
|
Resource description
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
The badge layout resource contains the following public fields:
|
||||||
|
|
||||||
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
Field Type Description
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
id integer Internal layout ID
|
||||||
|
name string Internal layout description
|
||||||
|
default boolean ``true`` if this is the default layout
|
||||||
|
layout object Layout specification for libpretixprint
|
||||||
|
background URL Background PDF file
|
||||||
|
item_assignments list of objects Products this layout is assigned to
|
||||||
|
└ item integer Item ID
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
.. versionchanged:: 1.16
|
||||||
|
|
||||||
|
This resource has been added.
|
||||||
|
|
||||||
|
|
||||||
|
Endpoints
|
||||||
|
---------
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/badgelayouts/
|
||||||
|
|
||||||
|
Returns a list of all badge layouts
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/democon/badgelayouts/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: text/javascript
|
||||||
|
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Default layout",
|
||||||
|
"default": true,
|
||||||
|
"layout": {…},
|
||||||
|
"background": {},
|
||||||
|
"item_assignments": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
:query page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:param organizer: The ``slug`` field of a valid organizer
|
||||||
|
:param event: The ``slug`` field of a valid event
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/badgelayouts/(id)/
|
||||||
|
|
||||||
|
Returns information on layout.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/democon/layoutsbadge/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: text/javascript
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Default layout",
|
||||||
|
"default": true,
|
||||||
|
"layout": {…},
|
||||||
|
"background": {},
|
||||||
|
"item_assignments": []
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param event: The ``slug`` field of the event to fetch
|
||||||
|
:param id: The ``id`` field of the layout 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 it.
|
||||||
@@ -12,3 +12,5 @@ If you want to **create** a plugin, please go to the
|
|||||||
list
|
list
|
||||||
pretixdroid
|
pretixdroid
|
||||||
banktransfer
|
banktransfer
|
||||||
|
ticketoutputpdf
|
||||||
|
badges
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ uses to communicate with the pretix server.
|
|||||||
"attention": false,
|
"attention": false,
|
||||||
"redeemed": true,
|
"redeemed": true,
|
||||||
"checkin_allowed": true,
|
"checkin_allowed": true,
|
||||||
|
"addons_text": "Parking spot",
|
||||||
"paid": true
|
"paid": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,6 +107,7 @@ uses to communicate with the pretix server.
|
|||||||
"attention": false,
|
"attention": false,
|
||||||
"redeemed": true,
|
"redeemed": true,
|
||||||
"checkin_allowed": true,
|
"checkin_allowed": true,
|
||||||
|
"addons_text": "Parking spot",
|
||||||
"paid": true
|
"paid": true
|
||||||
},
|
},
|
||||||
"questions": [
|
"questions": [
|
||||||
@@ -152,6 +154,7 @@ uses to communicate with the pretix server.
|
|||||||
"attention": false,
|
"attention": false,
|
||||||
"redeemed": true,
|
"redeemed": true,
|
||||||
"checkin_allowed": true,
|
"checkin_allowed": true,
|
||||||
|
"addons_text": "Parking spot",
|
||||||
"paid": true
|
"paid": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,6 +215,7 @@ uses to communicate with the pretix server.
|
|||||||
"redeemed": false,
|
"redeemed": false,
|
||||||
"attention": false,
|
"attention": false,
|
||||||
"checkin_allowed": true,
|
"checkin_allowed": true,
|
||||||
|
"addons_text": "Parking spot",
|
||||||
"paid": true
|
"paid": true
|
||||||
},
|
},
|
||||||
...
|
...
|
||||||
|
|||||||
111
doc/plugins/ticketoutputpdf.rst
Normal file
111
doc/plugins/ticketoutputpdf.rst
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
PDF ticket output
|
||||||
|
=================
|
||||||
|
|
||||||
|
The PDF ticket output plugin provides a HTTP API that exposes the various layouts used
|
||||||
|
to generate PDF tickets.
|
||||||
|
|
||||||
|
Resource description
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
The ticket layout resource contains the following public fields:
|
||||||
|
|
||||||
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
Field Type Description
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
id integer Internal layout ID
|
||||||
|
name string Internal layout description
|
||||||
|
default boolean ``true`` if this is the default layout
|
||||||
|
layout object Layout specification for libpretixprint
|
||||||
|
background URL Background PDF file
|
||||||
|
item_assignments list of objects Products this layout is assigned to
|
||||||
|
└ item integer Item ID
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
.. versionchanged:: 1.16
|
||||||
|
|
||||||
|
This resource has been added.
|
||||||
|
|
||||||
|
|
||||||
|
Endpoints
|
||||||
|
---------
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/ticketlayouts/
|
||||||
|
|
||||||
|
Returns a list of all ticket layouts
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/democon/ticketlayouts/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: text/javascript
|
||||||
|
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Default layout",
|
||||||
|
"default": true,
|
||||||
|
"layout": {…},
|
||||||
|
"background": {},
|
||||||
|
"item_assignments": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
:query page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:param organizer: The ``slug`` field of a valid organizer
|
||||||
|
:param event: The ``slug`` field of a valid event
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/ticketlayouts/(id)/
|
||||||
|
|
||||||
|
Returns information on layout.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/democon/ticketlayouts/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: text/javascript
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Default layout",
|
||||||
|
"default": true,
|
||||||
|
"layout": {…},
|
||||||
|
"background": {},
|
||||||
|
"item_assignments": []
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param event: The ``slug`` field of the event to fetch
|
||||||
|
:param id: The ``id`` field of the layout 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 it.
|
||||||
@@ -38,10 +38,12 @@ gunicorn
|
|||||||
hardcoded
|
hardcoded
|
||||||
hostname
|
hostname
|
||||||
idempotency
|
idempotency
|
||||||
|
incrementing
|
||||||
inofficial
|
inofficial
|
||||||
invalidations
|
invalidations
|
||||||
iterable
|
iterable
|
||||||
Jimdo
|
Jimdo
|
||||||
|
libpretixprint
|
||||||
libsass
|
libsass
|
||||||
linters
|
linters
|
||||||
memcached
|
memcached
|
||||||
@@ -77,6 +79,7 @@ prometheus
|
|||||||
proxied
|
proxied
|
||||||
proxying
|
proxying
|
||||||
pseudonymize
|
pseudonymize
|
||||||
|
pseudonymization
|
||||||
queryset
|
queryset
|
||||||
redemptions
|
redemptions
|
||||||
redis
|
redis
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "1.15.1"
|
__version__ = "1.17.0"
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class PretixApiConfig(AppConfig):
|
||||||
|
name = 'pretix.api'
|
||||||
|
label = 'pretixapi'
|
||||||
|
|
||||||
|
|
||||||
|
default_app_config = 'pretix.api.PretixApiConfig'
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from rest_framework.permissions import SAFE_METHODS, BasePermission
|
from rest_framework.permissions import SAFE_METHODS, BasePermission
|
||||||
|
|
||||||
|
from pretix.api.models import OAuthAccessToken
|
||||||
from pretix.base.models import Event
|
from pretix.base.models import Event
|
||||||
from pretix.base.models.organizer import Organizer, TeamAPIToken
|
from pretix.base.models.organizer import Organizer, TeamAPIToken
|
||||||
from pretix.helpers.security import (
|
from pretix.helpers.security import (
|
||||||
@@ -55,6 +56,15 @@ class EventPermission(BasePermission):
|
|||||||
|
|
||||||
if required_permission and required_permission not in request.orgapermset:
|
if required_permission and required_permission not in request.orgapermset:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if isinstance(request.auth, OAuthAccessToken):
|
||||||
|
if not request.auth.allow_scopes(['write']) and request.method not in SAFE_METHODS:
|
||||||
|
return False
|
||||||
|
if not request.auth.allow_scopes(['read']) and request.method in SAFE_METHODS:
|
||||||
|
return False
|
||||||
|
if isinstance(request.auth, OAuthAccessToken) and hasattr(request, 'organizer'):
|
||||||
|
if not request.auth.organizers.filter(pk=request.organizer.pk).exists():
|
||||||
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
128
src/pretix/api/migrations/0001_initial.py
Normal file
128
src/pretix/api/migrations/0001_initial.py
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.13 on 2018-06-04 11:19
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import oauth2_provider.generators
|
||||||
|
import oauth2_provider.validators
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='OAuthAccessToken',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
|
('token', models.CharField(max_length=255, unique=True)),
|
||||||
|
('expires', models.DateTimeField()),
|
||||||
|
('scope', models.TextField(blank=True)),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated', models.DateTimeField(auto_now=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='OAuthApplication',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
|
('client_type',
|
||||||
|
models.CharField(choices=[('confidential', 'Confidential'), ('public', 'Public')], max_length=32)),
|
||||||
|
('authorization_grant_type', models.CharField(
|
||||||
|
choices=[('authorization-code', 'Authorization code'), ('implicit', 'Implicit'),
|
||||||
|
('password', 'Resource owner password-based'),
|
||||||
|
('client-credentials', 'Client credentials')], max_length=32)),
|
||||||
|
('skip_authorization', models.BooleanField(default=False)),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated', models.DateTimeField(auto_now=True)),
|
||||||
|
('name', models.CharField(max_length=255, verbose_name='Application name')),
|
||||||
|
('redirect_uris', models.TextField(help_text='Allowed URIs list, space separated',
|
||||||
|
validators=[oauth2_provider.validators.validate_uris],
|
||||||
|
verbose_name='Redirection URIs')),
|
||||||
|
('client_id',
|
||||||
|
models.CharField(db_index=True, default=oauth2_provider.generators.generate_client_id, max_length=100,
|
||||||
|
unique=True, verbose_name='Client ID')),
|
||||||
|
('client_secret',
|
||||||
|
models.CharField(db_index=True, default=oauth2_provider.generators.generate_client_secret,
|
||||||
|
max_length=255, verbose_name='Client secret')),
|
||||||
|
('active', models.BooleanField(default=True)),
|
||||||
|
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name='pretixapi_oauthapplication', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='OAuthGrant',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
|
('code', models.CharField(max_length=255, unique=True)),
|
||||||
|
('expires', models.DateTimeField()),
|
||||||
|
('redirect_uri', models.CharField(max_length=255)),
|
||||||
|
('scope', models.TextField(blank=True)),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated', models.DateTimeField(auto_now=True)),
|
||||||
|
('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
|
||||||
|
('user',
|
||||||
|
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pretixapi_oauthgrant',
|
||||||
|
to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='OAuthRefreshToken',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
|
('token', models.CharField(max_length=255)),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated', models.DateTimeField(auto_now=True)),
|
||||||
|
('revoked', models.DateTimeField(null=True)),
|
||||||
|
('access_token',
|
||||||
|
models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name='refresh_token', to=settings.OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL)),
|
||||||
|
('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name='pretixapi_oauthrefreshtoken', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='oauthaccesstoken',
|
||||||
|
name='application',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='oauthaccesstoken',
|
||||||
|
name='source_refresh_token',
|
||||||
|
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name='refreshed_access_token',
|
||||||
|
to=settings.OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='oauthaccesstoken',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name='pretixapi_oauthaccesstoken', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='oauthrefreshtoken',
|
||||||
|
unique_together=set([('token', 'revoked')]),
|
||||||
|
),
|
||||||
|
]
|
||||||
26
src/pretix/api/migrations/0002_auto_20180604_1120.py
Normal file
26
src/pretix/api/migrations/0002_auto_20180604_1120.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.13 on 2018-06-04 11:20
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0001_initial'),
|
||||||
|
('pretixapi', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='oauthaccesstoken',
|
||||||
|
name='organizers',
|
||||||
|
field=models.ManyToManyField(to='pretixbase.Organizer'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='oauthgrant',
|
||||||
|
name='organizers',
|
||||||
|
field=models.ManyToManyField(to='pretixbase.Organizer'),
|
||||||
|
),
|
||||||
|
]
|
||||||
0
src/pretix/api/migrations/__init__.py
Normal file
0
src/pretix/api/migrations/__init__.py
Normal file
70
src/pretix/api/models.py
Normal file
70
src/pretix/api/models.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from oauth2_provider.generators import (
|
||||||
|
generate_client_id, generate_client_secret,
|
||||||
|
)
|
||||||
|
from oauth2_provider.models import (
|
||||||
|
AbstractAccessToken, AbstractApplication, AbstractGrant,
|
||||||
|
AbstractRefreshToken,
|
||||||
|
)
|
||||||
|
from oauth2_provider.validators import validate_uris
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthApplication(AbstractApplication):
|
||||||
|
name = models.CharField(verbose_name=_("Application name"), max_length=255, blank=False)
|
||||||
|
redirect_uris = models.TextField(
|
||||||
|
blank=False, validators=[validate_uris],
|
||||||
|
verbose_name=_("Redirection URIs"),
|
||||||
|
help_text=_("Allowed URIs list, space separated")
|
||||||
|
)
|
||||||
|
client_id = models.CharField(
|
||||||
|
verbose_name=_("Client ID"),
|
||||||
|
max_length=100, unique=True, default=generate_client_id, db_index=True
|
||||||
|
)
|
||||||
|
client_secret = models.CharField(
|
||||||
|
verbose_name=_("Client secret"),
|
||||||
|
max_length=255, blank=False, default=generate_client_secret, db_index=True
|
||||||
|
)
|
||||||
|
active = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse("control:user.settings.oauth.app", kwargs={'pk': self.id})
|
||||||
|
|
||||||
|
def is_usable(self, request):
|
||||||
|
return self.active and super().is_usable(request)
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthGrant(AbstractGrant):
|
||||||
|
application = models.ForeignKey(
|
||||||
|
OAuthApplication, on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
organizers = models.ManyToManyField('pretixbase.Organizer')
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthAccessToken(AbstractAccessToken):
|
||||||
|
source_refresh_token = models.OneToOneField(
|
||||||
|
# unique=True implied by the OneToOneField
|
||||||
|
'OAuthRefreshToken', on_delete=models.SET_NULL, blank=True, null=True,
|
||||||
|
related_name="refreshed_access_token"
|
||||||
|
)
|
||||||
|
application = models.ForeignKey(
|
||||||
|
OAuthApplication, on_delete=models.CASCADE, blank=True, null=True,
|
||||||
|
)
|
||||||
|
organizers = models.ManyToManyField('pretixbase.Organizer')
|
||||||
|
|
||||||
|
def revoke(self):
|
||||||
|
self.expires = now() - timedelta(hours=1)
|
||||||
|
self.save(update_fields=['expires'])
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthRefreshToken(AbstractRefreshToken):
|
||||||
|
application = models.ForeignKey(
|
||||||
|
OAuthApplication, on_delete=models.CASCADE)
|
||||||
|
access_token = models.OneToOneField(
|
||||||
|
OAuthAccessToken, on_delete=models.SET_NULL, blank=True, null=True,
|
||||||
|
related_name="refresh_token"
|
||||||
|
)
|
||||||
45
src/pretix/api/oauth.py
Normal file
45
src/pretix/api/oauth.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
|
from oauth2_provider.exceptions import FatalClientError
|
||||||
|
from oauth2_provider.oauth2_validators import Grant, OAuth2Validator
|
||||||
|
from oauth2_provider.settings import oauth2_settings
|
||||||
|
|
||||||
|
|
||||||
|
class Validator(OAuth2Validator):
|
||||||
|
|
||||||
|
def save_authorization_code(self, client_id, code, request, *args, **kwargs):
|
||||||
|
if not getattr(request, 'organizers', None):
|
||||||
|
raise FatalClientError('No organizers selected.')
|
||||||
|
|
||||||
|
expires = timezone.now() + timedelta(
|
||||||
|
seconds=oauth2_settings.AUTHORIZATION_CODE_EXPIRE_SECONDS)
|
||||||
|
g = Grant(application=request.client, user=request.user, code=code["code"],
|
||||||
|
expires=expires, redirect_uri=request.redirect_uri,
|
||||||
|
scope=" ".join(request.scopes))
|
||||||
|
g.save()
|
||||||
|
g.organizers.add(*request.organizers.all())
|
||||||
|
|
||||||
|
def validate_code(self, client_id, code, client, request, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
grant = Grant.objects.get(code=code, application=client)
|
||||||
|
if not grant.is_expired():
|
||||||
|
request.scopes = grant.scope.split(" ")
|
||||||
|
request.user = grant.user
|
||||||
|
request.organizers = grant.organizers.all()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Grant.DoesNotExist:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _create_access_token(self, expires, request, token, source_refresh_token=None):
|
||||||
|
if not getattr(request, 'organizers', None) and not getattr(source_refresh_token, 'access_token'):
|
||||||
|
raise FatalClientError('No organizers selected.')
|
||||||
|
if hasattr(request, 'organizers'):
|
||||||
|
orgs = list(request.organizers.all())
|
||||||
|
else:
|
||||||
|
orgs = list(source_refresh_token.access_token.organizers.all())
|
||||||
|
access_token = super()._create_access_token(expires, request, token, source_refresh_token=None)
|
||||||
|
access_token.organizers.add(*orgs)
|
||||||
|
return access_token
|
||||||
121
src/pretix/api/serializers/cart.py
Normal file
121
src/pretix/api/serializers/cart.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.utils.crypto import get_random_string
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from django.utils.translation import ugettext_lazy
|
||||||
|
from rest_framework import serializers
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
|
from pretix.api.serializers.order import (
|
||||||
|
AnswerCreateSerializer, AnswerSerializer,
|
||||||
|
)
|
||||||
|
from pretix.base.models import Quota
|
||||||
|
from pretix.base.models.orders import CartPosition
|
||||||
|
|
||||||
|
|
||||||
|
class CartPositionSerializer(I18nAwareModelSerializer):
|
||||||
|
answers = AnswerSerializer(many=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CartPosition
|
||||||
|
fields = ('id', 'cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_email',
|
||||||
|
'voucher', 'addon_to', 'subevent', 'datetime', 'expires', 'includes_tax',
|
||||||
|
'answers',)
|
||||||
|
|
||||||
|
|
||||||
|
class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||||
|
answers = AnswerCreateSerializer(many=True, required=False)
|
||||||
|
expires = serializers.DateTimeField(required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CartPosition
|
||||||
|
fields = ('cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_email',
|
||||||
|
'subevent', 'expires', 'includes_tax', 'answers',)
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
answers_data = validated_data.pop('answers')
|
||||||
|
if not validated_data.get('cart_id'):
|
||||||
|
cid = "{}@api".format(get_random_string(48))
|
||||||
|
while CartPosition.objects.filter(cart_id=cid).exists():
|
||||||
|
cid = "{}@api".format(get_random_string(48))
|
||||||
|
validated_data['cart_id'] = cid
|
||||||
|
|
||||||
|
if not validated_data.get('expires'):
|
||||||
|
validated_data['expires'] = now() + timedelta(
|
||||||
|
minutes=self.context['event'].settings.get('reservation_time', as_type=int)
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.context['event'].lock():
|
||||||
|
new_quotas = (validated_data.get('variation').quotas.filter(subevent=validated_data.get('subevent'))
|
||||||
|
if validated_data.get('variation')
|
||||||
|
else validated_data.get('item').quotas.filter(subevent=validated_data.get('subevent')))
|
||||||
|
if len(new_quotas) == 0:
|
||||||
|
raise ValidationError(
|
||||||
|
ugettext_lazy('The product "{}" is not assigned to a quota.').format(
|
||||||
|
str(validated_data.get('item'))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for quota in new_quotas:
|
||||||
|
avail = quota.availability()
|
||||||
|
if avail[0] != Quota.AVAILABILITY_OK or (avail[1] is not None and avail[1] < 1):
|
||||||
|
raise ValidationError(
|
||||||
|
ugettext_lazy('There is not enough quota available on quota "{}" to perform '
|
||||||
|
'the operation.').format(
|
||||||
|
quota.name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
cp = CartPosition.objects.create(event=self.context['event'], **validated_data)
|
||||||
|
|
||||||
|
for answ_data in answers_data:
|
||||||
|
options = answ_data.pop('options')
|
||||||
|
answ = cp.answers.create(**answ_data)
|
||||||
|
answ.options.add(*options)
|
||||||
|
return cp
|
||||||
|
|
||||||
|
def validate_cart_id(self, cid):
|
||||||
|
if cid and not cid.endswith('@api'):
|
||||||
|
raise ValidationError('Cart ID should end in @api or be empty.')
|
||||||
|
|
||||||
|
def validate_item(self, item):
|
||||||
|
if item.event != self.context['event']:
|
||||||
|
raise ValidationError(
|
||||||
|
'The specified item does not belong to this event.'
|
||||||
|
)
|
||||||
|
if not item.active:
|
||||||
|
raise ValidationError(
|
||||||
|
'The specified item is not active.'
|
||||||
|
)
|
||||||
|
return item
|
||||||
|
|
||||||
|
def validate_subevent(self, subevent):
|
||||||
|
if self.context['event'].has_subevents:
|
||||||
|
if not subevent:
|
||||||
|
raise ValidationError(
|
||||||
|
'You need to set a subevent.'
|
||||||
|
)
|
||||||
|
if subevent.event != self.context['event']:
|
||||||
|
raise ValidationError(
|
||||||
|
'The specified subevent does not belong to this event.'
|
||||||
|
)
|
||||||
|
elif subevent:
|
||||||
|
raise ValidationError(
|
||||||
|
'You cannot set a subevent for this event.'
|
||||||
|
)
|
||||||
|
return subevent
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
if data.get('item'):
|
||||||
|
if data.get('item').has_variations:
|
||||||
|
if not data.get('variation'):
|
||||||
|
raise ValidationError('You should specify a variation for this item.')
|
||||||
|
else:
|
||||||
|
if data.get('variation').item != data.get('item'):
|
||||||
|
raise ValidationError(
|
||||||
|
'The specified variation does not belong to the specified item.'
|
||||||
|
)
|
||||||
|
elif data.get('variation'):
|
||||||
|
raise ValidationError(
|
||||||
|
'You cannot specify a variation for this item.'
|
||||||
|
)
|
||||||
|
return data
|
||||||
@@ -74,12 +74,12 @@ class ItemSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Item
|
model = Item
|
||||||
fields = ('id', 'category', 'name', 'active', 'description',
|
fields = ('id', 'category', 'name', 'internal_name', 'active', 'description',
|
||||||
'default_price', 'free_price', 'tax_rate', 'tax_rule', 'admission',
|
'default_price', 'free_price', 'tax_rate', 'tax_rule', 'admission',
|
||||||
'position', 'picture', 'available_from', 'available_until',
|
'position', 'picture', 'available_from', 'available_until',
|
||||||
'require_voucher', 'hide_without_voucher', 'allow_cancel',
|
'require_voucher', 'hide_without_voucher', 'allow_cancel',
|
||||||
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations',
|
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations',
|
||||||
'variations', 'addons')
|
'variations', 'addons', 'original_price')
|
||||||
read_only_fields = ('has_variations', 'picture')
|
read_only_fields = ('has_variations', 'picture')
|
||||||
|
|
||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
@@ -129,7 +129,7 @@ class ItemCategorySerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ItemCategory
|
model = ItemCategory
|
||||||
fields = ('id', 'name', 'description', 'position', 'is_addon')
|
fields = ('id', 'name', 'internal_name', 'description', 'position', 'is_addon')
|
||||||
|
|
||||||
|
|
||||||
class QuestionOptionSerializer(I18nAwareModelSerializer):
|
class QuestionOptionSerializer(I18nAwareModelSerializer):
|
||||||
|
|||||||
@@ -1,18 +1,28 @@
|
|||||||
|
import json
|
||||||
|
from collections import Counter
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from django.utils.translation import ugettext_lazy
|
||||||
|
from django_countries.fields import Country
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
|
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Checkin, Invoice, InvoiceAddress, InvoiceLine, Order, OrderPosition,
|
Checkin, Invoice, InvoiceAddress, InvoiceLine, Order, OrderPosition,
|
||||||
QuestionAnswer,
|
Question, QuestionAnswer,
|
||||||
)
|
)
|
||||||
from pretix.base.models.orders import OrderFee
|
from pretix.base.models.orders import CartPosition, OrderFee
|
||||||
|
from pretix.base.pdf import get_variables
|
||||||
from pretix.base.signals import register_ticket_outputs
|
from pretix.base.signals import register_ticket_outputs
|
||||||
|
|
||||||
|
|
||||||
class CompatibleCountryField(serializers.Field):
|
class CompatibleCountryField(serializers.Field):
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
return {self.field_name: Country(data)}
|
||||||
|
|
||||||
def to_representation(self, instance: InvoiceAddress):
|
def to_representation(self, instance: InvoiceAddress):
|
||||||
if instance.country:
|
if instance.country:
|
||||||
return str(instance.country)
|
return str(instance.country)
|
||||||
@@ -27,6 +37,13 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
|
|||||||
model = InvoiceAddress
|
model = InvoiceAddress
|
||||||
fields = ('last_modified', 'is_business', 'company', 'name', 'street', 'zipcode', 'city', 'country', 'vat_id',
|
fields = ('last_modified', 'is_business', 'company', 'name', 'street', 'zipcode', 'city', 'country', 'vat_id',
|
||||||
'vat_id_validated', 'internal_reference')
|
'vat_id_validated', 'internal_reference')
|
||||||
|
read_only_fields = ('last_modified', 'vat_id_validated')
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
for v in self.fields.values():
|
||||||
|
v.required = False
|
||||||
|
v.allow_blank = True
|
||||||
|
|
||||||
|
|
||||||
class AnswerQuestionIdentifierField(serializers.Field):
|
class AnswerQuestionIdentifierField(serializers.Field):
|
||||||
@@ -104,17 +121,39 @@ class PositionDownloadsField(serializers.Field):
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
class PdfDataSerializer(serializers.Field):
|
||||||
|
def to_representation(self, instance: OrderPosition):
|
||||||
|
res = {}
|
||||||
|
|
||||||
|
ev = instance.subevent or instance.order.event
|
||||||
|
|
||||||
|
pdfvars = get_variables(instance.order.event)
|
||||||
|
for k, f in pdfvars.items():
|
||||||
|
res[k] = f['evaluate'](instance, instance.order, ev)
|
||||||
|
|
||||||
|
for k, v in ev.meta_data.items():
|
||||||
|
res['meta:' + k] = v
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
class OrderPositionSerializer(I18nAwareModelSerializer):
|
class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||||
checkins = CheckinSerializer(many=True)
|
checkins = CheckinSerializer(many=True)
|
||||||
answers = AnswerSerializer(many=True)
|
answers = AnswerSerializer(many=True)
|
||||||
downloads = PositionDownloadsField(source='*')
|
downloads = PositionDownloadsField(source='*')
|
||||||
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
|
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
|
||||||
|
pdf_data = PdfDataSerializer(source='*')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = OrderPosition
|
model = OrderPosition
|
||||||
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_email',
|
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_email',
|
||||||
'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins', 'downloads',
|
'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins', 'downloads',
|
||||||
'answers', 'tax_rule')
|
'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data')
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if 'request' in self.context and not self.context['request'].query_params.get('pdf_data', 'false') == 'true':
|
||||||
|
self.fields.pop('pdf_data')
|
||||||
|
|
||||||
|
|
||||||
class OrderFeeSerializer(I18nAwareModelSerializer):
|
class OrderFeeSerializer(I18nAwareModelSerializer):
|
||||||
@@ -123,18 +162,6 @@ class OrderFeeSerializer(I18nAwareModelSerializer):
|
|||||||
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule')
|
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule')
|
||||||
|
|
||||||
|
|
||||||
class PaymentFeeLegacyField(serializers.Field):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self.attr = kwargs.pop('attribute')
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def to_representation(self, instance: Order):
|
|
||||||
return str(
|
|
||||||
sum([getattr(f, self.attr) for f in instance.fees.all() if f.fee_type == OrderFee.FEE_TYPE_PAYMENT],
|
|
||||||
Decimal('0.00'))
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class OrderSerializer(I18nAwareModelSerializer):
|
class OrderSerializer(I18nAwareModelSerializer):
|
||||||
invoice_address = InvoiceAddressSerializer()
|
invoice_address = InvoiceAddressSerializer()
|
||||||
positions = OrderPositionSerializer(many=True)
|
positions = OrderPositionSerializer(many=True)
|
||||||
@@ -145,7 +172,337 @@ class OrderSerializer(I18nAwareModelSerializer):
|
|||||||
model = Order
|
model = Order
|
||||||
fields = ('code', 'status', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date',
|
fields = ('code', 'status', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date',
|
||||||
'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads',
|
'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads',
|
||||||
'checkin_attention')
|
'checkin_attention', 'last_modified')
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if not self.context['request'].query_params.get('pdf_data', 'false') == 'true':
|
||||||
|
self.fields['positions'].child.fields.pop('pdf_data')
|
||||||
|
|
||||||
|
|
||||||
|
class AnswerCreateSerializer(I18nAwareModelSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = QuestionAnswer
|
||||||
|
fields = ('question', 'answer', 'options')
|
||||||
|
|
||||||
|
def validate_question(self, q):
|
||||||
|
if q.event != self.context['event']:
|
||||||
|
raise ValidationError(
|
||||||
|
'The specified question does not belong to this event.'
|
||||||
|
)
|
||||||
|
return q
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
if data.get('question').type == Question.TYPE_FILE:
|
||||||
|
raise ValidationError(
|
||||||
|
'File uploads are currently not supported via the API.'
|
||||||
|
)
|
||||||
|
elif data.get('question').type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
|
||||||
|
if not data.get('options'):
|
||||||
|
raise ValidationError(
|
||||||
|
'You need to specify options if the question is of a choice type.'
|
||||||
|
)
|
||||||
|
if data.get('question').type == Question.TYPE_CHOICE and len(data.get('options')) > 1:
|
||||||
|
raise ValidationError(
|
||||||
|
'You can specify at most one option for this question.'
|
||||||
|
)
|
||||||
|
data['answer'] = ", ".join([str(o) for o in data.get('options')])
|
||||||
|
|
||||||
|
else:
|
||||||
|
if data.get('options'):
|
||||||
|
raise ValidationError(
|
||||||
|
'You should not specify options if the question is not of a choice type.'
|
||||||
|
)
|
||||||
|
|
||||||
|
if data.get('question').type == Question.TYPE_BOOLEAN:
|
||||||
|
if data.get('answer') in ['true', 'True', '1', 'TRUE']:
|
||||||
|
data['answer'] = 'True'
|
||||||
|
elif data.get('answer') in ['false', 'False', '0', 'FALSE']:
|
||||||
|
data['answer'] = 'False'
|
||||||
|
else:
|
||||||
|
raise ValidationError(
|
||||||
|
'Please specify "true" or "false" for boolean questions.'
|
||||||
|
)
|
||||||
|
elif data.get('question').type == Question.TYPE_NUMBER:
|
||||||
|
serializers.DecimalField(
|
||||||
|
max_digits=50,
|
||||||
|
decimal_places=25
|
||||||
|
).to_internal_value(data.get('answer'))
|
||||||
|
elif data.get('question').type == Question.TYPE_DATE:
|
||||||
|
data['answer'] = serializers.DateField().to_internal_value(data.get('answer'))
|
||||||
|
elif data.get('question').type == Question.TYPE_TIME:
|
||||||
|
data['answer'] = serializers.TimeField().to_internal_value(data.get('answer'))
|
||||||
|
elif data.get('question').type == Question.TYPE_DATETIME:
|
||||||
|
data['answer'] = serializers.DateTimeField().to_internal_value(data.get('answer'))
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class OrderFeeCreateSerializer(I18nAwareModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = OrderFee
|
||||||
|
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rule')
|
||||||
|
|
||||||
|
def validate_tax_rule(self, tr):
|
||||||
|
if tr and tr.event != self.context['event']:
|
||||||
|
raise ValidationError(
|
||||||
|
'The specified tax rate does not belong to this event.'
|
||||||
|
)
|
||||||
|
return tr
|
||||||
|
|
||||||
|
|
||||||
|
class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
||||||
|
answers = AnswerCreateSerializer(many=True, required=False)
|
||||||
|
addon_to = serializers.IntegerField(required=False, allow_null=True)
|
||||||
|
secret = serializers.CharField(required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = OrderPosition
|
||||||
|
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_email',
|
||||||
|
'secret', 'addon_to', 'subevent', 'answers')
|
||||||
|
|
||||||
|
def validate_secret(self, secret):
|
||||||
|
if secret and OrderPosition.objects.filter(order__event=self.context['event'], secret=secret).exists():
|
||||||
|
raise ValidationError(
|
||||||
|
'You cannot assign a position secret that already exists.'
|
||||||
|
)
|
||||||
|
return secret
|
||||||
|
|
||||||
|
def validate_item(self, item):
|
||||||
|
if item.event != self.context['event']:
|
||||||
|
raise ValidationError(
|
||||||
|
'The specified item does not belong to this event.'
|
||||||
|
)
|
||||||
|
if not item.active:
|
||||||
|
raise ValidationError(
|
||||||
|
'The specified item is not active.'
|
||||||
|
)
|
||||||
|
return item
|
||||||
|
|
||||||
|
def validate_subevent(self, subevent):
|
||||||
|
if self.context['event'].has_subevents:
|
||||||
|
if not subevent:
|
||||||
|
raise ValidationError(
|
||||||
|
'You need to set a subevent.'
|
||||||
|
)
|
||||||
|
if subevent.event != self.context['event']:
|
||||||
|
raise ValidationError(
|
||||||
|
'The specified subevent does not belong to this event.'
|
||||||
|
)
|
||||||
|
elif subevent:
|
||||||
|
raise ValidationError(
|
||||||
|
'You cannot set a subevent for this event.'
|
||||||
|
)
|
||||||
|
return subevent
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
if data.get('item'):
|
||||||
|
if data.get('item').has_variations:
|
||||||
|
if not data.get('variation'):
|
||||||
|
raise ValidationError({'variation': ['You should specify a variation for this item.']})
|
||||||
|
else:
|
||||||
|
if data.get('variation').item != data.get('item'):
|
||||||
|
raise ValidationError(
|
||||||
|
{'variation': ['The specified variation does not belong to the specified item.']}
|
||||||
|
)
|
||||||
|
elif data.get('variation'):
|
||||||
|
raise ValidationError(
|
||||||
|
{'variation': ['You cannot specify a variation for this item.']}
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class CompatibleJSONField(serializers.JSONField):
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
try:
|
||||||
|
return json.dumps(data)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
self.fail('invalid')
|
||||||
|
|
||||||
|
def to_representation(self, value):
|
||||||
|
if value:
|
||||||
|
return json.loads(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||||
|
invoice_address = InvoiceAddressSerializer(required=False)
|
||||||
|
positions = OrderPositionCreateSerializer(many=True, required=False)
|
||||||
|
fees = OrderFeeCreateSerializer(many=True, required=False)
|
||||||
|
status = serializers.ChoiceField(choices=(
|
||||||
|
('n', Order.STATUS_PENDING),
|
||||||
|
('p', Order.STATUS_PAID),
|
||||||
|
), default='n', required=False)
|
||||||
|
code = serializers.CharField(
|
||||||
|
required=False,
|
||||||
|
max_length=16,
|
||||||
|
min_length=5
|
||||||
|
)
|
||||||
|
comment = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
payment_provider = serializers.CharField(required=True)
|
||||||
|
payment_info = CompatibleJSONField(required=False)
|
||||||
|
consume_carts = serializers.ListField(child=serializers.CharField(), required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Order
|
||||||
|
fields = ('code', 'status', 'email', 'locale', 'payment_provider', 'fees', 'comment',
|
||||||
|
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'consume_carts')
|
||||||
|
|
||||||
|
def validate_payment_provider(self, pp):
|
||||||
|
if pp not in self.context['event'].get_payment_providers():
|
||||||
|
raise ValidationError('The given payment provider is not known.')
|
||||||
|
return pp
|
||||||
|
|
||||||
|
def validate_code(self, code):
|
||||||
|
if code and Order.objects.filter(event__organizer=self.context['event'].organizer, code=code).exists():
|
||||||
|
raise ValidationError(
|
||||||
|
'This order code is already in use.'
|
||||||
|
)
|
||||||
|
if any(c not in 'ABCDEFGHJKLMNPQRSTUVWXYZ1234567890' for c in code):
|
||||||
|
raise ValidationError(
|
||||||
|
'This order code contains invalid characters.'
|
||||||
|
)
|
||||||
|
return code
|
||||||
|
|
||||||
|
def validate_positions(self, data):
|
||||||
|
if not data:
|
||||||
|
raise ValidationError(
|
||||||
|
'An order cannot be empty.'
|
||||||
|
)
|
||||||
|
errs = [{} for p in data]
|
||||||
|
if any([p.get('positionid') for p in data]):
|
||||||
|
if not all([p.get('positionid') for p in data]):
|
||||||
|
for i, p in enumerate(data):
|
||||||
|
if not p.get('positionid'):
|
||||||
|
errs[i]['positionid'] = [
|
||||||
|
'If you set position IDs manually, you need to do so for all positions.'
|
||||||
|
]
|
||||||
|
raise ValidationError(errs)
|
||||||
|
|
||||||
|
last_non_add_on = None
|
||||||
|
last_posid = 0
|
||||||
|
|
||||||
|
for i, p in enumerate(data):
|
||||||
|
if p['positionid'] != last_posid + 1:
|
||||||
|
errs[i]['positionid'] = [
|
||||||
|
'Position IDs need to be consecutive.'
|
||||||
|
]
|
||||||
|
if p.get('addon_to') and p['addon_to'] != last_non_add_on:
|
||||||
|
errs[i]['addon_to'] = [
|
||||||
|
"If you set addon_to, you need to make sure that the referenced "
|
||||||
|
"position ID exists and is transmitted directly before its add-ons."
|
||||||
|
]
|
||||||
|
|
||||||
|
if not p.get('addon_to'):
|
||||||
|
last_non_add_on = p['positionid']
|
||||||
|
last_posid = p['positionid']
|
||||||
|
|
||||||
|
elif any([p.get('addon_to') for p in data]):
|
||||||
|
errs = [
|
||||||
|
{'positionid': ["If you set addon_to on any position, you need to specify position IDs manually."]}
|
||||||
|
for p in data
|
||||||
|
]
|
||||||
|
|
||||||
|
if any(errs):
|
||||||
|
raise ValidationError(errs)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
fees_data = validated_data.pop('fees') if 'fees' in validated_data else []
|
||||||
|
positions_data = validated_data.pop('positions') if 'positions' in validated_data else []
|
||||||
|
if 'invoice_address' in validated_data:
|
||||||
|
ia = InvoiceAddress(**validated_data.pop('invoice_address'))
|
||||||
|
else:
|
||||||
|
ia = None
|
||||||
|
|
||||||
|
with self.context['event'].lock() as now_dt:
|
||||||
|
quotadiff = Counter()
|
||||||
|
|
||||||
|
consume_carts = validated_data.pop('consume_carts', [])
|
||||||
|
delete_cps = []
|
||||||
|
quota_avail_cache = {}
|
||||||
|
if consume_carts:
|
||||||
|
for cp in CartPosition.objects.filter(event=self.context['event'], cart_id__in=consume_carts):
|
||||||
|
quotas = (cp.variation.quotas.filter(subevent=cp.subevent)
|
||||||
|
if cp.variation else cp.item.quotas.filter(subevent=cp.subevent))
|
||||||
|
for quota in quotas:
|
||||||
|
if quota not in quota_avail_cache:
|
||||||
|
quota_avail_cache[quota] = list(quota.availability())
|
||||||
|
if quota_avail_cache[quota][1] is not None:
|
||||||
|
quota_avail_cache[quota][1] += 1
|
||||||
|
if cp.expires > now_dt:
|
||||||
|
quotadiff.subtract(quotas)
|
||||||
|
delete_cps.append(cp)
|
||||||
|
|
||||||
|
errs = [{} for p in positions_data]
|
||||||
|
|
||||||
|
for i, pos_data in enumerate(positions_data):
|
||||||
|
new_quotas = (pos_data.get('variation').quotas.filter(subevent=pos_data.get('subevent'))
|
||||||
|
if pos_data.get('variation')
|
||||||
|
else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent')))
|
||||||
|
if len(new_quotas) == 0:
|
||||||
|
errs[i]['item'] = [ugettext_lazy('The product "{}" is not assigned to a quota.').format(
|
||||||
|
str(pos_data.get('item'))
|
||||||
|
)]
|
||||||
|
else:
|
||||||
|
for quota in new_quotas:
|
||||||
|
if quota not in quota_avail_cache:
|
||||||
|
quota_avail_cache[quota] = list(quota.availability())
|
||||||
|
|
||||||
|
if quota_avail_cache[quota][1] is not None:
|
||||||
|
quota_avail_cache[quota][1] -= 1
|
||||||
|
if quota_avail_cache[quota][1] < 0:
|
||||||
|
errs[i]['item'] = [
|
||||||
|
ugettext_lazy('There is not enough quota available on quota "{}" to perform the operation.').format(
|
||||||
|
quota.name
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
quotadiff.update(new_quotas)
|
||||||
|
|
||||||
|
if any(errs):
|
||||||
|
raise ValidationError({'positions': errs})
|
||||||
|
|
||||||
|
order = Order(event=self.context['event'], **validated_data)
|
||||||
|
order.set_expires(subevents=[p['subevent'] for p in positions_data])
|
||||||
|
order.total = sum([p['price'] for p in positions_data]) + sum([f['value'] for f in fees_data], Decimal('0.00'))
|
||||||
|
order.meta_info = "{}"
|
||||||
|
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID:
|
||||||
|
order.payment_provider = 'free'
|
||||||
|
order.status = Order.STATUS_PAID
|
||||||
|
elif order.payment_provider == "free" and order.total != Decimal('0.00'):
|
||||||
|
raise ValidationError('You cannot use the "free" payment provider for non-free orders.')
|
||||||
|
if validated_data.get('status') == Order.STATUS_PAID:
|
||||||
|
order.payment_date = now()
|
||||||
|
order.save()
|
||||||
|
if ia:
|
||||||
|
ia.order = order
|
||||||
|
ia.save()
|
||||||
|
pos_map = {}
|
||||||
|
for pos_data in positions_data:
|
||||||
|
answers_data = pos_data.pop('answers')
|
||||||
|
addon_to = pos_data.pop('addon_to')
|
||||||
|
pos = OrderPosition(**pos_data)
|
||||||
|
pos.order = order
|
||||||
|
pos._calculate_tax()
|
||||||
|
if addon_to:
|
||||||
|
pos.addon_to = pos_map[addon_to]
|
||||||
|
pos.save()
|
||||||
|
pos_map[pos.positionid] = pos
|
||||||
|
for answ_data in answers_data:
|
||||||
|
options = answ_data.pop('options')
|
||||||
|
answ = pos.answers.create(**answ_data)
|
||||||
|
answ.options.add(*options)
|
||||||
|
|
||||||
|
for cp in delete_cps:
|
||||||
|
cp.delete()
|
||||||
|
for fee_data in fees_data:
|
||||||
|
f = OrderFee(**fee_data)
|
||||||
|
f.order = order
|
||||||
|
f._calculate_tax()
|
||||||
|
f.save()
|
||||||
|
|
||||||
|
return order
|
||||||
|
|
||||||
|
|
||||||
class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
|
class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ from django.apps import apps
|
|||||||
from django.conf.urls import include, url
|
from django.conf.urls import include, url
|
||||||
from rest_framework import routers
|
from rest_framework import routers
|
||||||
|
|
||||||
from .views import checkin, event, item, order, organizer, voucher, waitinglist
|
from pretix.api.views import cart
|
||||||
|
|
||||||
|
from .views import (
|
||||||
|
checkin, event, item, oauth, order, organizer, voucher, waitinglist,
|
||||||
|
)
|
||||||
|
|
||||||
router = routers.DefaultRouter()
|
router = routers.DefaultRouter()
|
||||||
router.register(r'organizers', organizer.OrganizerViewSet)
|
router.register(r'organizers', organizer.OrganizerViewSet)
|
||||||
@@ -26,6 +30,7 @@ event_router.register(r'invoices', order.InvoiceViewSet)
|
|||||||
event_router.register(r'taxrules', event.TaxRuleViewSet)
|
event_router.register(r'taxrules', event.TaxRuleViewSet)
|
||||||
event_router.register(r'waitinglistentries', waitinglist.WaitingListViewSet)
|
event_router.register(r'waitinglistentries', waitinglist.WaitingListViewSet)
|
||||||
event_router.register(r'checkinlists', checkin.CheckinListViewSet)
|
event_router.register(r'checkinlists', checkin.CheckinListViewSet)
|
||||||
|
event_router.register(r'cartpositions', cart.CartPositionViewSet)
|
||||||
|
|
||||||
checkinlist_router = routers.DefaultRouter()
|
checkinlist_router = routers.DefaultRouter()
|
||||||
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet)
|
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet)
|
||||||
@@ -52,4 +57,7 @@ urlpatterns = [
|
|||||||
include(question_router.urls)),
|
include(question_router.urls)),
|
||||||
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/checkinlists/(?P<list>[^/]+)/',
|
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/checkinlists/(?P<list>[^/]+)/',
|
||||||
include(checkinlist_router.urls)),
|
include(checkinlist_router.urls)),
|
||||||
|
url(r"^oauth/authorize$", oauth.AuthorizationView.as_view(), name="authorize"),
|
||||||
|
url(r"^oauth/token$", oauth.TokenView.as_view(), name="token"),
|
||||||
|
url(r"^oauth/revoke_token$", oauth.RevokeTokenView.as_view(), name="revoke-token"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
from calendar import timegm
|
||||||
|
|
||||||
|
from django.db.models import Max
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.utils.http import http_date, parse_http_date_safe
|
||||||
from rest_framework.filters import OrderingFilter
|
from rest_framework.filters import OrderingFilter
|
||||||
|
|
||||||
|
|
||||||
@@ -21,3 +26,33 @@ class RichOrderingFilter(OrderingFilter):
|
|||||||
return queryset.order_by(*ordering)
|
return queryset.order_by(*ordering)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
class ConditionalListView:
|
||||||
|
|
||||||
|
def list(self, request, **kwargs):
|
||||||
|
if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
|
||||||
|
if if_modified_since:
|
||||||
|
if_modified_since = parse_http_date_safe(if_modified_since)
|
||||||
|
if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
|
||||||
|
if if_unmodified_since:
|
||||||
|
if_unmodified_since = parse_http_date_safe(if_unmodified_since)
|
||||||
|
lmd = request.event.logentry_set.filter(
|
||||||
|
content_type__model=self.queryset.model._meta.model_name,
|
||||||
|
content_type__app_label=self.queryset.model._meta.app_label,
|
||||||
|
).aggregate(
|
||||||
|
m=Max('datetime')
|
||||||
|
)['m']
|
||||||
|
if lmd:
|
||||||
|
lmd_ts = timegm(lmd.utctimetuple())
|
||||||
|
|
||||||
|
if if_unmodified_since and lmd and lmd_ts > if_unmodified_since:
|
||||||
|
return HttpResponse(status=412)
|
||||||
|
|
||||||
|
if if_modified_since and lmd and lmd_ts <= if_modified_since:
|
||||||
|
return HttpResponse(status=304)
|
||||||
|
|
||||||
|
resp = super().list(request, **kwargs)
|
||||||
|
if lmd:
|
||||||
|
resp['Last-Modified'] = http_date(lmd_ts)
|
||||||
|
return resp
|
||||||
|
|||||||
46
src/pretix/api/views/cart.py
Normal file
46
src/pretix/api/views/cart.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
from django.db import transaction
|
||||||
|
from rest_framework import status, viewsets
|
||||||
|
from rest_framework.filters import OrderingFilter
|
||||||
|
from rest_framework.mixins import CreateModelMixin, DestroyModelMixin
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from pretix.api.serializers.cart import (
|
||||||
|
CartPositionCreateSerializer, CartPositionSerializer,
|
||||||
|
)
|
||||||
|
from pretix.base.models import CartPosition
|
||||||
|
|
||||||
|
|
||||||
|
class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||||
|
serializer_class = CartPositionSerializer
|
||||||
|
queryset = CartPosition.objects.none()
|
||||||
|
filter_backends = (OrderingFilter,)
|
||||||
|
ordering = ('datetime',)
|
||||||
|
ordering_fields = ('datetime', 'cart_id')
|
||||||
|
lookup_field = 'id'
|
||||||
|
permission = 'can_view_orders'
|
||||||
|
write_permission = 'can_change_orders'
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return CartPosition.objects.filter(
|
||||||
|
event=self.request.event,
|
||||||
|
cart_id__endswith="@api"
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
ctx = super().get_serializer_context()
|
||||||
|
ctx['event'] = self.request.event
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
serializer = CartPositionCreateSerializer(data=request.data, context=self.get_serializer_context())
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
with transaction.atomic():
|
||||||
|
self.perform_create(serializer)
|
||||||
|
cp = serializer.instance
|
||||||
|
serializer = CartPositionSerializer(cp, context=serializer.context)
|
||||||
|
|
||||||
|
headers = self.get_success_headers(serializer.data)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
serializer.save()
|
||||||
@@ -16,7 +16,6 @@ from pretix.api.serializers.order import OrderPositionSerializer
|
|||||||
from pretix.api.views import RichOrderingFilter
|
from pretix.api.views import RichOrderingFilter
|
||||||
from pretix.api.views.order import OrderPositionFilter
|
from pretix.api.views.order import OrderPositionFilter
|
||||||
from pretix.base.models import Checkin, CheckinList, Order, OrderPosition
|
from pretix.base.models import Checkin, CheckinList, Order, OrderPosition
|
||||||
from pretix.base.models.organizer import TeamAPIToken
|
|
||||||
from pretix.base.services.checkin import (
|
from pretix.base.services.checkin import (
|
||||||
CheckInError, RequiredQuestionsError, perform_checkin,
|
CheckInError, RequiredQuestionsError, perform_checkin,
|
||||||
)
|
)
|
||||||
@@ -49,7 +48,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.checkinlist.added',
|
'pretix.event.checkinlist.added',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -63,7 +62,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.checkinlist.changed',
|
'pretix.event.checkinlist.changed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -71,7 +70,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
|||||||
instance.log_action(
|
instance.log_action(
|
||||||
'pretix.event.checkinlist.deleted',
|
'pretix.event.checkinlist.deleted',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
)
|
)
|
||||||
super().perform_destroy(instance)
|
super().perform_destroy(instance)
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ from pretix.api.serializers.event import (
|
|||||||
CloneEventSerializer, EventSerializer, SubEventSerializer,
|
CloneEventSerializer, EventSerializer, SubEventSerializer,
|
||||||
TaxRuleSerializer,
|
TaxRuleSerializer,
|
||||||
)
|
)
|
||||||
|
from pretix.api.views import ConditionalListView
|
||||||
from pretix.base.models import Event, ItemCategory, TaxRule
|
from pretix.base.models import Event, ItemCategory, TaxRule
|
||||||
from pretix.base.models.event import SubEvent
|
from pretix.base.models.event import SubEvent
|
||||||
from pretix.base.models.organizer import TeamAPIToken
|
|
||||||
from pretix.helpers.dicts import merge_dicts
|
from pretix.helpers.dicts import merge_dicts
|
||||||
|
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ class EventViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
log_action,
|
log_action,
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ class EventViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.plugins.' + action,
|
'pretix.event.plugins.' + action,
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data={'plugin': module}
|
data={'plugin': module}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ class EventViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.changed',
|
'pretix.event.changed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ class EventViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.added',
|
'pretix.event.added',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ class CloneEventViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.added',
|
'pretix.event.added',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ class SubEventFilter(FilterSet):
|
|||||||
fields = ['active']
|
fields = ['active']
|
||||||
|
|
||||||
|
|
||||||
class SubEventViewSet(viewsets.ReadOnlyModelViewSet):
|
class SubEventViewSet(ConditionalListView, viewsets.ReadOnlyModelViewSet):
|
||||||
serializer_class = SubEventSerializer
|
serializer_class = SubEventSerializer
|
||||||
queryset = ItemCategory.objects.none()
|
queryset = ItemCategory.objects.none()
|
||||||
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
|
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
|
||||||
@@ -137,7 +137,7 @@ class SubEventViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TaxRuleViewSet(viewsets.ModelViewSet):
|
class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||||
serializer_class = TaxRuleSerializer
|
serializer_class = TaxRuleSerializer
|
||||||
queryset = TaxRule.objects.none()
|
queryset = TaxRule.objects.none()
|
||||||
write_permission = 'can_change_event_settings'
|
write_permission = 'can_change_event_settings'
|
||||||
@@ -150,7 +150,7 @@ class TaxRuleViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.taxrule.changed',
|
'pretix.event.taxrule.changed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ class TaxRuleViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.taxrule.added',
|
'pretix.event.taxrule.added',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -170,6 +170,6 @@ class TaxRuleViewSet(viewsets.ModelViewSet):
|
|||||||
instance.log_action(
|
instance.log_action(
|
||||||
'pretix.event.taxrule.deleted',
|
'pretix.event.taxrule.deleted',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
)
|
)
|
||||||
super().perform_destroy(instance)
|
super().perform_destroy(instance)
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ from pretix.api.serializers.item import (
|
|||||||
ItemVariationSerializer, QuestionOptionSerializer, QuestionSerializer,
|
ItemVariationSerializer, QuestionOptionSerializer, QuestionSerializer,
|
||||||
QuotaSerializer,
|
QuotaSerializer,
|
||||||
)
|
)
|
||||||
|
from pretix.api.views import ConditionalListView
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Item, ItemAddOn, ItemCategory, ItemVariation, Question, QuestionOption,
|
Item, ItemAddOn, ItemCategory, ItemVariation, Question, QuestionOption,
|
||||||
Quota,
|
Quota,
|
||||||
)
|
)
|
||||||
from pretix.base.models.organizer import TeamAPIToken
|
|
||||||
from pretix.helpers.dicts import merge_dicts
|
from pretix.helpers.dicts import merge_dicts
|
||||||
|
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ class ItemFilter(FilterSet):
|
|||||||
fields = ['active', 'category', 'admission', 'tax_rate', 'free_price']
|
fields = ['active', 'category', 'admission', 'tax_rate', 'free_price']
|
||||||
|
|
||||||
|
|
||||||
class ItemViewSet(viewsets.ModelViewSet):
|
class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||||
serializer_class = ItemSerializer
|
serializer_class = ItemSerializer
|
||||||
queryset = Item.objects.none()
|
queryset = Item.objects.none()
|
||||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||||
@@ -53,7 +53,7 @@ class ItemViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.item.added',
|
'pretix.event.item.added',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ class ItemViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.item.changed',
|
'pretix.event.item.changed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ class ItemViewSet(viewsets.ModelViewSet):
|
|||||||
instance.log_action(
|
instance.log_action(
|
||||||
'pretix.event.item.deleted',
|
'pretix.event.item.deleted',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
)
|
)
|
||||||
super().perform_destroy(instance)
|
super().perform_destroy(instance)
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
|||||||
item.log_action(
|
item.log_action(
|
||||||
'pretix.event.item.variation.added',
|
'pretix.event.item.variation.added',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk},
|
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk},
|
||||||
{'value': serializer.instance.value})
|
{'value': serializer.instance.value})
|
||||||
)
|
)
|
||||||
@@ -123,7 +123,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.item.log_action(
|
serializer.instance.item.log_action(
|
||||||
'pretix.event.item.variation.changed',
|
'pretix.event.item.variation.changed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk},
|
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk},
|
||||||
{'value': serializer.instance.value})
|
{'value': serializer.instance.value})
|
||||||
)
|
)
|
||||||
@@ -140,7 +140,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
|||||||
instance.item.log_action(
|
instance.item.log_action(
|
||||||
'pretix.event.item.variation.deleted',
|
'pretix.event.item.variation.deleted',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data={
|
data={
|
||||||
'value': instance.value,
|
'value': instance.value,
|
||||||
'id': self.kwargs['pk']
|
'id': self.kwargs['pk']
|
||||||
@@ -174,7 +174,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
|
|||||||
item.log_action(
|
item.log_action(
|
||||||
'pretix.event.item.addons.added',
|
'pretix.event.item.addons.added',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
|
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -183,7 +183,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.base_item.log_action(
|
serializer.instance.base_item.log_action(
|
||||||
'pretix.event.item.addons.changed',
|
'pretix.event.item.addons.changed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
|
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -192,7 +192,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
|
|||||||
instance.base_item.log_action(
|
instance.base_item.log_action(
|
||||||
'pretix.event.item.addons.removed',
|
'pretix.event.item.addons.removed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data={'category': instance.addon_category.pk}
|
data={'category': instance.addon_category.pk}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -203,7 +203,7 @@ class ItemCategoryFilter(FilterSet):
|
|||||||
fields = ['is_addon']
|
fields = ['is_addon']
|
||||||
|
|
||||||
|
|
||||||
class ItemCategoryViewSet(viewsets.ModelViewSet):
|
class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||||
serializer_class = ItemCategorySerializer
|
serializer_class = ItemCategorySerializer
|
||||||
queryset = ItemCategory.objects.none()
|
queryset = ItemCategory.objects.none()
|
||||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||||
@@ -221,7 +221,7 @@ class ItemCategoryViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.category.added',
|
'pretix.event.category.added',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -235,7 +235,7 @@ class ItemCategoryViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.category.changed',
|
'pretix.event.category.changed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -246,7 +246,7 @@ class ItemCategoryViewSet(viewsets.ModelViewSet):
|
|||||||
instance.log_action(
|
instance.log_action(
|
||||||
'pretix.event.category.deleted',
|
'pretix.event.category.deleted',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
)
|
)
|
||||||
super().perform_destroy(instance)
|
super().perform_destroy(instance)
|
||||||
|
|
||||||
@@ -257,7 +257,7 @@ class QuestionFilter(FilterSet):
|
|||||||
fields = ['ask_during_checkin', 'required', 'identifier']
|
fields = ['ask_during_checkin', 'required', 'identifier']
|
||||||
|
|
||||||
|
|
||||||
class QuestionViewSet(viewsets.ModelViewSet):
|
class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||||
serializer_class = QuestionSerializer
|
serializer_class = QuestionSerializer
|
||||||
queryset = Question.objects.none()
|
queryset = Question.objects.none()
|
||||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||||
@@ -274,7 +274,7 @@ class QuestionViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.question.added',
|
'pretix.event.question.added',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -288,7 +288,7 @@ class QuestionViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.question.changed',
|
'pretix.event.question.changed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -296,7 +296,7 @@ class QuestionViewSet(viewsets.ModelViewSet):
|
|||||||
instance.log_action(
|
instance.log_action(
|
||||||
'pretix.event.question.deleted',
|
'pretix.event.question.deleted',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
)
|
)
|
||||||
super().perform_destroy(instance)
|
super().perform_destroy(instance)
|
||||||
|
|
||||||
@@ -326,7 +326,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
|||||||
q.log_action(
|
q.log_action(
|
||||||
'pretix.event.question.option.added',
|
'pretix.event.question.option.added',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
|
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -335,7 +335,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.question.log_action(
|
serializer.instance.question.log_action(
|
||||||
'pretix.event.question.option.changed',
|
'pretix.event.question.option.changed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
|
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -343,7 +343,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
|||||||
instance.question.log_action(
|
instance.question.log_action(
|
||||||
'pretix.event.question.option.deleted',
|
'pretix.event.question.option.deleted',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data={'id': instance.pk}
|
data={'id': instance.pk}
|
||||||
)
|
)
|
||||||
super().perform_destroy(instance)
|
super().perform_destroy(instance)
|
||||||
@@ -355,7 +355,7 @@ class QuotaFilter(FilterSet):
|
|||||||
fields = ['subevent']
|
fields = ['subevent']
|
||||||
|
|
||||||
|
|
||||||
class QuotaViewSet(viewsets.ModelViewSet):
|
class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||||
serializer_class = QuotaSerializer
|
serializer_class = QuotaSerializer
|
||||||
queryset = Quota.objects.none()
|
queryset = Quota.objects.none()
|
||||||
filter_backends = (DjangoFilterBackend, OrderingFilter,)
|
filter_backends = (DjangoFilterBackend, OrderingFilter,)
|
||||||
@@ -373,14 +373,14 @@ class QuotaViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.quota.added',
|
'pretix.event.quota.added',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
if serializer.instance.subevent:
|
if serializer.instance.subevent:
|
||||||
serializer.instance.subevent.log_action(
|
serializer.instance.subevent.log_action(
|
||||||
'pretix.subevent.quota.added',
|
'pretix.subevent.quota.added',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -396,7 +396,7 @@ class QuotaViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.quota.changed',
|
'pretix.event.quota.changed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
if current_subevent == request_subevent:
|
if current_subevent == request_subevent:
|
||||||
@@ -404,7 +404,7 @@ class QuotaViewSet(viewsets.ModelViewSet):
|
|||||||
current_subevent.log_action(
|
current_subevent.log_action(
|
||||||
'pretix.subevent.quota.changed',
|
'pretix.subevent.quota.changed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -412,14 +412,14 @@ class QuotaViewSet(viewsets.ModelViewSet):
|
|||||||
request_subevent.log_action(
|
request_subevent.log_action(
|
||||||
'pretix.subevent.quota.added',
|
'pretix.subevent.quota.added',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
if current_subevent is not None:
|
if current_subevent is not None:
|
||||||
current_subevent.log_action(
|
current_subevent.log_action(
|
||||||
'pretix.subevent.quota.deleted',
|
'pretix.subevent.quota.deleted',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
)
|
)
|
||||||
serializer.instance.rebuild_cache()
|
serializer.instance.rebuild_cache()
|
||||||
|
|
||||||
@@ -427,13 +427,13 @@ class QuotaViewSet(viewsets.ModelViewSet):
|
|||||||
instance.log_action(
|
instance.log_action(
|
||||||
'pretix.event.quota.deleted',
|
'pretix.event.quota.deleted',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
)
|
)
|
||||||
if instance.subevent:
|
if instance.subevent:
|
||||||
instance.subevent.log_action(
|
instance.subevent.log_action(
|
||||||
'pretix.subevent.quota.deleted',
|
'pretix.subevent.quota.deleted',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
)
|
)
|
||||||
super().perform_destroy(instance)
|
super().perform_destroy(instance)
|
||||||
|
|
||||||
|
|||||||
92
src/pretix/api/views/oauth.py
Normal file
92
src/pretix/api/views/oauth.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
from oauth2_provider.exceptions import OAuthToolkitError
|
||||||
|
from oauth2_provider.forms import AllowForm
|
||||||
|
from oauth2_provider.views import (
|
||||||
|
AuthorizationView as BaseAuthorizationView,
|
||||||
|
RevokeTokenView as BaseRevokeTokenView, TokenView as BaseTokenView,
|
||||||
|
)
|
||||||
|
|
||||||
|
from pretix.api.models import OAuthApplication
|
||||||
|
from pretix.base.models import Organizer
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthAllowForm(AllowForm):
|
||||||
|
organizers = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=Organizer.objects.none(),
|
||||||
|
widget=forms.CheckboxSelectMultiple
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
user = kwargs.pop('user')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['organizers'].queryset = Organizer.objects.filter(
|
||||||
|
pk__in=user.teams.values_list('organizer', flat=True))
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorizationView(BaseAuthorizationView):
|
||||||
|
template_name = "pretixcontrol/auth/oauth_authorization.html"
|
||||||
|
form_class = OAuthAllowForm
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
kwargs = super().get_form_kwargs()
|
||||||
|
kwargs['user'] = self.request.user
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
ctx['settings'] = settings
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
def create_authorization_response(self, request, scopes, credentials, allow, organizers):
|
||||||
|
credentials["organizers"] = organizers
|
||||||
|
return super().create_authorization_response(request, scopes, credentials, allow)
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
client_id = form.cleaned_data["client_id"]
|
||||||
|
application = OAuthApplication.objects.get(client_id=client_id)
|
||||||
|
credentials = {
|
||||||
|
"client_id": form.cleaned_data.get("client_id"),
|
||||||
|
"redirect_uri": form.cleaned_data.get("redirect_uri"),
|
||||||
|
"response_type": form.cleaned_data.get("response_type", None),
|
||||||
|
"state": form.cleaned_data.get("state", None),
|
||||||
|
}
|
||||||
|
scopes = form.cleaned_data.get("scope")
|
||||||
|
allow = form.cleaned_data.get("allow")
|
||||||
|
|
||||||
|
try:
|
||||||
|
uri, headers, body, status = self.create_authorization_response(
|
||||||
|
request=self.request, scopes=scopes, credentials=credentials, allow=allow,
|
||||||
|
organizers=form.cleaned_data.get("organizers")
|
||||||
|
)
|
||||||
|
except OAuthToolkitError as error:
|
||||||
|
return self.error_response(error, application)
|
||||||
|
|
||||||
|
self.success_url = uri
|
||||||
|
logger.debug("Success url for the request: {0}".format(self.success_url))
|
||||||
|
|
||||||
|
msgs = [
|
||||||
|
_('The application "{application_name}" has been authorized to access your account.').format(
|
||||||
|
application_name=application.name
|
||||||
|
)
|
||||||
|
]
|
||||||
|
self.request.user.send_security_notice(msgs)
|
||||||
|
self.request.user.log_action('pretix.user.oauth.authorized', user=self.request.user, data={
|
||||||
|
'application_id': application.pk,
|
||||||
|
'application_name': application.name,
|
||||||
|
})
|
||||||
|
|
||||||
|
return self.redirect(self.success_url, application)
|
||||||
|
|
||||||
|
|
||||||
|
class TokenView(BaseTokenView):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RevokeTokenView(BaseRevokeTokenView):
|
||||||
|
pass
|
||||||
@@ -2,10 +2,11 @@ import datetime
|
|||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
import pytz
|
import pytz
|
||||||
|
from django.db import transaction
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.db.models.functions import Concat
|
from django.db.models.functions import Concat
|
||||||
from django.http import FileResponse
|
from django.http import FileResponse
|
||||||
from django.utils.timezone import make_aware
|
from django.utils.timezone import make_aware, now
|
||||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
from rest_framework import serializers, status, viewsets
|
from rest_framework import serializers, status, viewsets
|
||||||
from rest_framework.decorators import detail_route
|
from rest_framework.decorators import detail_route
|
||||||
@@ -13,38 +14,44 @@ from rest_framework.exceptions import (
|
|||||||
APIException, NotFound, PermissionDenied, ValidationError,
|
APIException, NotFound, PermissionDenied, ValidationError,
|
||||||
)
|
)
|
||||||
from rest_framework.filters import OrderingFilter
|
from rest_framework.filters import OrderingFilter
|
||||||
|
from rest_framework.mixins import CreateModelMixin
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from pretix.api.models import OAuthAccessToken
|
||||||
from pretix.api.serializers.order import (
|
from pretix.api.serializers.order import (
|
||||||
InvoiceSerializer, OrderPositionSerializer, OrderSerializer,
|
InvoiceSerializer, OrderCreateSerializer, OrderPositionSerializer,
|
||||||
|
OrderSerializer,
|
||||||
|
)
|
||||||
|
from pretix.base.models import (
|
||||||
|
Invoice, Order, OrderPosition, Quota, TeamAPIToken,
|
||||||
)
|
)
|
||||||
from pretix.base.models import Invoice, Order, OrderPosition, Quota
|
|
||||||
from pretix.base.models.organizer import TeamAPIToken
|
|
||||||
from pretix.base.services.invoices import (
|
from pretix.base.services.invoices import (
|
||||||
generate_cancellation, generate_invoice, invoice_pdf, regenerate_invoice,
|
generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified,
|
||||||
|
regenerate_invoice,
|
||||||
)
|
)
|
||||||
from pretix.base.services.mail import SendMailException
|
from pretix.base.services.mail import SendMailException
|
||||||
from pretix.base.services.orders import (
|
from pretix.base.services.orders import (
|
||||||
OrderError, cancel_order, extend_order, mark_order_expired,
|
OrderError, cancel_order, extend_order, mark_order_expired,
|
||||||
mark_order_paid,
|
mark_order_paid, mark_order_refunded,
|
||||||
)
|
)
|
||||||
from pretix.base.services.tickets import (
|
from pretix.base.services.tickets import (
|
||||||
get_cachedticket_for_order, get_cachedticket_for_position,
|
get_cachedticket_for_order, get_cachedticket_for_position,
|
||||||
)
|
)
|
||||||
from pretix.base.signals import register_ticket_outputs
|
from pretix.base.signals import order_placed, register_ticket_outputs
|
||||||
|
|
||||||
|
|
||||||
class OrderFilter(FilterSet):
|
class OrderFilter(FilterSet):
|
||||||
email = django_filters.CharFilter(name='email', lookup_expr='iexact')
|
email = django_filters.CharFilter(name='email', lookup_expr='iexact')
|
||||||
code = django_filters.CharFilter(name='code', lookup_expr='iexact')
|
code = django_filters.CharFilter(name='code', lookup_expr='iexact')
|
||||||
status = django_filters.CharFilter(name='status', lookup_expr='iexact')
|
status = django_filters.CharFilter(name='status', lookup_expr='iexact')
|
||||||
|
modified_since = django_filters.IsoDateTimeFilter(name='last_modified', lookup_expr='gte')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Order
|
model = Order
|
||||||
fields = ['code', 'status', 'email', 'locale']
|
fields = ['code', 'status', 'email', 'locale']
|
||||||
|
|
||||||
|
|
||||||
class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||||
serializer_class = OrderSerializer
|
serializer_class = OrderSerializer
|
||||||
queryset = Order.objects.none()
|
queryset = Order.objects.none()
|
||||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||||
@@ -55,6 +62,11 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
permission = 'can_view_orders'
|
permission = 'can_view_orders'
|
||||||
write_permission = 'can_change_orders'
|
write_permission = 'can_change_orders'
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
ctx = super().get_serializer_context()
|
||||||
|
ctx['event'] = self.request.event
|
||||||
|
return ctx
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.event.orders.prefetch_related(
|
return self.request.event.orders.prefetch_related(
|
||||||
'positions', 'positions__checkins', 'positions__item', 'positions__answers', 'positions__answers__options',
|
'positions', 'positions__checkins', 'positions__item', 'positions__answers', 'positions__answers__options',
|
||||||
@@ -71,6 +83,20 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
return prov
|
return prov
|
||||||
raise NotFound('Unknown output provider.')
|
raise NotFound('Unknown output provider.')
|
||||||
|
|
||||||
|
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})
|
||||||
|
|
||||||
@detail_route(url_name='download', url_path='download/(?P<output>[^/]+)')
|
@detail_route(url_name='download', url_path='download/(?P<output>[^/]+)')
|
||||||
def download(self, request, output, **kwargs):
|
def download(self, request, output, **kwargs):
|
||||||
provider = self._get_output_provider(output)
|
provider = self._get_output_provider(output)
|
||||||
@@ -100,7 +126,7 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
mark_order_paid(
|
mark_order_paid(
|
||||||
order, manual=True,
|
order, manual=True,
|
||||||
user=request.user if request.user.is_authenticated else None,
|
user=request.user if request.user.is_authenticated else None,
|
||||||
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
|
auth=request.auth,
|
||||||
)
|
)
|
||||||
except Quota.QuotaExceededException as e:
|
except Quota.QuotaExceededException as e:
|
||||||
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
@@ -127,7 +153,8 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
cancel_order(
|
cancel_order(
|
||||||
order,
|
order,
|
||||||
user=request.user if request.user.is_authenticated else None,
|
user=request.user if request.user.is_authenticated else None,
|
||||||
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
|
api_token=request.auth if isinstance(request.auth, TeamAPIToken) else None,
|
||||||
|
oauth_application=request.auth.application if isinstance(request.auth, OAuthAccessToken) else None,
|
||||||
send_mail=send_mail
|
send_mail=send_mail
|
||||||
)
|
)
|
||||||
return self.retrieve(request, [], **kwargs)
|
return self.retrieve(request, [], **kwargs)
|
||||||
@@ -148,7 +175,7 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
order.log_action(
|
order.log_action(
|
||||||
'pretix.event.order.unpaid',
|
'pretix.event.order.unpaid',
|
||||||
user=request.user if request.user.is_authenticated else None,
|
user=request.user if request.user.is_authenticated else None,
|
||||||
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
|
auth=request.auth,
|
||||||
)
|
)
|
||||||
return self.retrieve(request, [], **kwargs)
|
return self.retrieve(request, [], **kwargs)
|
||||||
|
|
||||||
@@ -165,11 +192,26 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
mark_order_expired(
|
mark_order_expired(
|
||||||
order,
|
order,
|
||||||
user=request.user if request.user.is_authenticated else None,
|
user=request.user if request.user.is_authenticated else None,
|
||||||
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
|
auth=request.auth,
|
||||||
)
|
)
|
||||||
return self.retrieve(request, [], **kwargs)
|
return self.retrieve(request, [], **kwargs)
|
||||||
|
|
||||||
# TODO: Find a way to implement mark_refunded
|
@detail_route(methods=['POST'])
|
||||||
|
def mark_refunded(self, request, **kwargs):
|
||||||
|
order = self.get_object()
|
||||||
|
|
||||||
|
if order.status != Order.STATUS_PAID:
|
||||||
|
return Response(
|
||||||
|
{'detail': 'The order is not paid.'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
mark_order_refunded(
|
||||||
|
order,
|
||||||
|
user=request.user if request.user.is_authenticated else None,
|
||||||
|
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
|
||||||
|
)
|
||||||
|
return self.retrieve(request, [], **kwargs)
|
||||||
|
|
||||||
@detail_route(methods=['POST'])
|
@detail_route(methods=['POST'])
|
||||||
def extend(self, request, **kwargs):
|
def extend(self, request, **kwargs):
|
||||||
@@ -204,7 +246,7 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
new_date=new_date,
|
new_date=new_date,
|
||||||
force=force,
|
force=force,
|
||||||
user=request.user if request.user.is_authenticated else None,
|
user=request.user if request.user.is_authenticated else None,
|
||||||
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
|
auth=request.auth,
|
||||||
)
|
)
|
||||||
return self.retrieve(request, [], **kwargs)
|
return self.retrieve(request, [], **kwargs)
|
||||||
except OrderError as e:
|
except OrderError as e:
|
||||||
@@ -213,6 +255,34 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
status=status.HTTP_400_BAD_REQUEST
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
serializer = OrderCreateSerializer(data=request.data, context=self.get_serializer_context())
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
with transaction.atomic():
|
||||||
|
self.perform_create(serializer)
|
||||||
|
order = serializer.instance
|
||||||
|
serializer = OrderSerializer(order, context=serializer.context)
|
||||||
|
|
||||||
|
order.log_action(
|
||||||
|
'pretix.event.order.placed',
|
||||||
|
user=request.user if request.user.is_authenticated else None,
|
||||||
|
auth=request.auth,
|
||||||
|
)
|
||||||
|
order_placed.send(self.request.event, order=order)
|
||||||
|
|
||||||
|
gen_invoice = invoice_qualified(order) and (
|
||||||
|
(order.event.settings.get('invoice_generate') == 'True') or
|
||||||
|
(order.event.settings.get('invoice_generate') == 'paid' and order.status == Order.STATUS_PAID)
|
||||||
|
) and not order.invoices.last()
|
||||||
|
if gen_invoice:
|
||||||
|
generate_invoice(order, trigger_pdf=True)
|
||||||
|
|
||||||
|
headers = self.get_success_headers(serializer.data)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
serializer.save()
|
||||||
|
|
||||||
|
|
||||||
class OrderPositionFilter(FilterSet):
|
class OrderPositionFilter(FilterSet):
|
||||||
order = django_filters.CharFilter(name='order', lookup_expr='code__iexact')
|
order = django_filters.CharFilter(name='order', lookup_expr='code__iexact')
|
||||||
@@ -370,7 +440,7 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
'invoice': inv.pk
|
'invoice': inv.pk
|
||||||
},
|
},
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
)
|
)
|
||||||
return Response(status=204)
|
return Response(status=204)
|
||||||
|
|
||||||
@@ -393,6 +463,6 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
'invoice': inv.pk
|
'invoice': inv.pk
|
||||||
},
|
},
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
)
|
)
|
||||||
return Response(status=204)
|
return Response(status=204)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
|
|
||||||
|
from pretix.api.models import OAuthAccessToken
|
||||||
from pretix.api.serializers.organizer import OrganizerSerializer
|
from pretix.api.serializers.organizer import OrganizerSerializer
|
||||||
from pretix.base.models import Organizer
|
from pretix.base.models import Organizer
|
||||||
|
|
||||||
@@ -14,6 +15,12 @@ class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
if self.request.user.is_authenticated():
|
if self.request.user.is_authenticated():
|
||||||
if self.request.user.has_active_staff_session(self.request.session.session_key):
|
if self.request.user.has_active_staff_session(self.request.session.session_key):
|
||||||
return Organizer.objects.all()
|
return Organizer.objects.all()
|
||||||
|
elif isinstance(self.request.auth, OAuthAccessToken):
|
||||||
|
return Organizer.objects.filter(
|
||||||
|
pk__in=self.request.user.teams.values_list('organizer', flat=True)
|
||||||
|
).filter(
|
||||||
|
pk__in=self.request.auth.organizers.values_list('pk', flat=True)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return Organizer.objects.filter(pk__in=self.request.user.teams.values_list('organizer', flat=True))
|
return Organizer.objects.filter(pk__in=self.request.user.teams.values_list('organizer', flat=True))
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ from rest_framework.filters import OrderingFilter
|
|||||||
|
|
||||||
from pretix.api.serializers.voucher import VoucherSerializer
|
from pretix.api.serializers.voucher import VoucherSerializer
|
||||||
from pretix.base.models import Voucher
|
from pretix.base.models import Voucher
|
||||||
from pretix.base.models.organizer import TeamAPIToken
|
|
||||||
|
|
||||||
|
|
||||||
class VoucherFilter(FilterSet):
|
class VoucherFilter(FilterSet):
|
||||||
@@ -51,7 +50,7 @@ class VoucherViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.voucher.added',
|
'pretix.voucher.added',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -69,7 +68,7 @@ class VoucherViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.voucher.changed',
|
'pretix.voucher.changed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -80,6 +79,6 @@ class VoucherViewSet(viewsets.ModelViewSet):
|
|||||||
instance.log_action(
|
instance.log_action(
|
||||||
'pretix.voucher.deleted',
|
'pretix.voucher.deleted',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
)
|
)
|
||||||
super().perform_destroy(instance)
|
super().perform_destroy(instance)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from rest_framework.filters import OrderingFilter
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from pretix.api.serializers.waitinglist import WaitingListSerializer
|
from pretix.api.serializers.waitinglist import WaitingListSerializer
|
||||||
from pretix.base.models import TeamAPIToken, WaitingListEntry
|
from pretix.base.models import WaitingListEntry
|
||||||
from pretix.base.models.waitinglist import WaitingListException
|
from pretix.base.models.waitinglist import WaitingListException
|
||||||
|
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ class WaitingListViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.orders.waitinglist.added',
|
'pretix.event.orders.waitinglist.added',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
)
|
)
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
@@ -55,7 +55,7 @@ class WaitingListViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.orders.waitinglist.changed',
|
'pretix.event.orders.waitinglist.changed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
)
|
)
|
||||||
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
@@ -65,7 +65,7 @@ class WaitingListViewSet(viewsets.ModelViewSet):
|
|||||||
instance.log_action(
|
instance.log_action(
|
||||||
'pretix.event.orders.waitinglist.deleted',
|
'pretix.event.orders.waitinglist.deleted',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
)
|
)
|
||||||
super().perform_destroy(instance)
|
super().perform_destroy(instance)
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ class WaitingListViewSet(viewsets.ModelViewSet):
|
|||||||
try:
|
try:
|
||||||
self.get_object().send_voucher(
|
self.get_object().send_voucher(
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
)
|
)
|
||||||
except WaitingListException as e:
|
except WaitingListException as e:
|
||||||
raise ValidationError(str(e))
|
raise ValidationError(str(e))
|
||||||
|
|||||||
@@ -38,12 +38,19 @@ class InvoiceExporter(BaseExporter):
|
|||||||
with tempfile.TemporaryDirectory() as d:
|
with tempfile.TemporaryDirectory() as d:
|
||||||
with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
|
with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
|
||||||
for i in qs:
|
for i in qs:
|
||||||
if not i.file:
|
try:
|
||||||
|
if not i.file:
|
||||||
|
invoice_pdf_task.apply(args=(i.pk,))
|
||||||
|
i.refresh_from_db()
|
||||||
|
i.file.open('rb')
|
||||||
|
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
|
||||||
|
i.file.close()
|
||||||
|
except FileNotFoundError:
|
||||||
invoice_pdf_task.apply(args=(i.pk,))
|
invoice_pdf_task.apply(args=(i.pk,))
|
||||||
i.refresh_from_db()
|
i.refresh_from_db()
|
||||||
i.file.open('rb')
|
i.file.open('rb')
|
||||||
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
|
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
|
||||||
i.file.close()
|
i.file.close()
|
||||||
|
|
||||||
with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
|
with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
|
||||||
return '{}_invoices.zip'.format(self.event.slug), 'application/zip', zipf.read()
|
return '{}_invoices.zip'.format(self.event.slug), 'application/zip', zipf.read()
|
||||||
|
|||||||
@@ -24,13 +24,15 @@ class JSONExporter(BaseExporter):
|
|||||||
'categories': [
|
'categories': [
|
||||||
{
|
{
|
||||||
'id': category.id,
|
'id': category.id,
|
||||||
'name': str(category.name)
|
'name': str(category.name),
|
||||||
|
'internal_name': category.internal_name
|
||||||
} for category in self.event.categories.all()
|
} for category in self.event.categories.all()
|
||||||
],
|
],
|
||||||
'items': [
|
'items': [
|
||||||
{
|
{
|
||||||
'id': item.id,
|
'id': item.id,
|
||||||
'name': str(item.name),
|
'name': str(item.name),
|
||||||
|
'internal_name': str(item.internal_name),
|
||||||
'category': item.category_id,
|
'category': item.category_id,
|
||||||
'price': item.default_price,
|
'price': item.default_price,
|
||||||
'tax_rate': item.tax_rule.rate if item.tax_rule else Decimal('0.00'),
|
'tax_rate': item.tax_rule.rate if item.tax_rule else Decimal('0.00'),
|
||||||
|
|||||||
@@ -242,3 +242,12 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
'resolve this manually.'))
|
'resolve this manually.'))
|
||||||
else:
|
else:
|
||||||
self.instance.vat_id_validated = False
|
self.instance.vat_id_validated = False
|
||||||
|
|
||||||
|
|
||||||
|
class BaseInvoiceNameForm(BaseInvoiceAddressForm):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
for f in list(self.fields.keys()):
|
||||||
|
if f != 'name':
|
||||||
|
del self.fields[f]
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
@@ -8,6 +9,7 @@ from django.contrib.staticfiles import finders
|
|||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.formats import date_format, localize
|
from django.utils.formats import date_format, localize
|
||||||
from django.utils.translation import pgettext
|
from django.utils.translation import pgettext
|
||||||
|
from PIL.Image import BICUBIC
|
||||||
from reportlab.lib import pagesizes
|
from reportlab.lib import pagesizes
|
||||||
from reportlab.lib.enums import TA_LEFT
|
from reportlab.lib.enums import TA_LEFT
|
||||||
from reportlab.lib.styles import ParagraphStyle, StyleSheet1
|
from reportlab.lib.styles import ParagraphStyle, StyleSheet1
|
||||||
@@ -26,6 +28,8 @@ from pretix.base.models import Event, Invoice
|
|||||||
from pretix.base.signals import register_invoice_renderers
|
from pretix.base.signals import register_invoice_renderers
|
||||||
from pretix.base.templatetags.money import money_filter
|
from pretix.base.templatetags.money import money_filter
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class BaseInvoiceRenderer:
|
class BaseInvoiceRenderer:
|
||||||
"""
|
"""
|
||||||
@@ -178,6 +182,14 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
|
|||||||
return 'invoice.pdf', 'application/pdf', buffer.read()
|
return 'invoice.pdf', 'application/pdf', buffer.read()
|
||||||
|
|
||||||
|
|
||||||
|
class ThumbnailingImageReader(ImageReader):
|
||||||
|
def resize(self, width, height, dpi):
|
||||||
|
self._image.thumbnail(
|
||||||
|
size=(int(width * dpi / 72), int(height * dpi / 72)),
|
||||||
|
resample=BICUBIC
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||||
identifier = 'classic'
|
identifier = 'classic'
|
||||||
verbose_name = pgettext('invoice', 'Classic renderer (pretix 1.0)')
|
verbose_name = pgettext('invoice', 'Classic renderer (pretix 1.0)')
|
||||||
@@ -276,25 +288,42 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
|
|
||||||
if self.invoice.event.settings.invoice_logo_image:
|
if self.invoice.event.settings.invoice_logo_image:
|
||||||
logo_file = self.invoice.event.settings.get('invoice_logo_image', binary_file=True)
|
logo_file = self.invoice.event.settings.get('invoice_logo_image', binary_file=True)
|
||||||
canvas.drawImage(ImageReader(logo_file),
|
ir = ThumbnailingImageReader(logo_file)
|
||||||
|
try:
|
||||||
|
ir.resize(25 * mm, 25 * mm, 300)
|
||||||
|
except:
|
||||||
|
logger.exception("Can not resize image")
|
||||||
|
pass
|
||||||
|
canvas.drawImage(ir,
|
||||||
95 * mm, (297 - 38) * mm,
|
95 * mm, (297 - 38) * mm,
|
||||||
width=25 * mm, height=25 * mm,
|
width=25 * mm, height=25 * mm,
|
||||||
preserveAspectRatio=True, anchor='n',
|
preserveAspectRatio=True, anchor='n',
|
||||||
mask='auto')
|
mask='auto')
|
||||||
|
|
||||||
|
def shorten(txt):
|
||||||
|
txt = str(txt)
|
||||||
|
p = Paragraph(txt.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
|
||||||
|
p_size = p.wrap(65 * mm, 50 * mm)
|
||||||
|
|
||||||
|
while p_size[1] > 2 * self.stylesheet['Normal'].leading:
|
||||||
|
txt = ' '.join(txt.replace('…', '').split()[:-1]) + '…'
|
||||||
|
p = Paragraph(txt.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
|
||||||
|
p_size = p.wrap(65 * mm, 50 * mm)
|
||||||
|
return txt
|
||||||
|
|
||||||
if not self.invoice.event.has_subevents:
|
if not self.invoice.event.has_subevents:
|
||||||
if self.invoice.event.settings.show_date_to:
|
if self.invoice.event.settings.show_date_to:
|
||||||
p_str = (
|
p_str = (
|
||||||
str(self.invoice.event.name) + '\n' + pgettext('invoice', '{from_date}\nuntil {to_date}').format(
|
shorten(self.invoice.event.name) + '\n' + pgettext('invoice', '{from_date}\nuntil {to_date}').format(
|
||||||
from_date=self.invoice.event.get_date_from_display(),
|
from_date=self.invoice.event.get_date_from_display(),
|
||||||
to_date=self.invoice.event.get_date_to_display())
|
to_date=self.invoice.event.get_date_to_display())
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
p_str = (
|
p_str = (
|
||||||
str(self.invoice.event.name) + '\n' + self.invoice.event.get_date_from_display()
|
shorten(self.invoice.event.name) + '\n' + self.invoice.event.get_date_from_display()
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
p_str = str(self.invoice.event.name)
|
p_str = shorten(self.invoice.event.name)
|
||||||
|
|
||||||
p = Paragraph(p_str.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
|
p = Paragraph(p_str.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
|
||||||
p.wrapOn(canvas, 65 * mm, 50 * mm)
|
p.wrapOn(canvas, 65 * mm, 50 * mm)
|
||||||
|
|||||||
@@ -172,6 +172,12 @@ class SecurityMiddleware(MiddlewareMixin):
|
|||||||
return resp
|
return resp
|
||||||
|
|
||||||
resp['X-XSS-Protection'] = '1'
|
resp['X-XSS-Protection'] = '1'
|
||||||
|
|
||||||
|
# We just need to have a P3P, not matter whats in there
|
||||||
|
# https://blogs.msdn.microsoft.com/ieinternals/2013/09/17/a-quick-look-at-p3p/
|
||||||
|
# https://github.com/pretix/pretix/issues/765
|
||||||
|
resp['P3P'] = 'CP=\"ALL DSP COR CUR ADM TAI OUR IND COM NAV INT\"'
|
||||||
|
|
||||||
h = {
|
h = {
|
||||||
'default-src': ["{static}"],
|
'default-src': ["{static}"],
|
||||||
'script-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
|
'script-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
|
||||||
|
|||||||
29
src/pretix/base/migrations/0090_auto_20180509_0917.py
Normal file
29
src/pretix/base/migrations/0090_auto_20180509_0917.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.11 on 2018-05-09 09:17
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0089_auto_20180315_1322'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='item',
|
||||||
|
name='internal_name',
|
||||||
|
field=models.CharField(blank=True,
|
||||||
|
help_text='If you set this, this will be used instead of the public name in the '
|
||||||
|
'backend.',
|
||||||
|
max_length=255, null=True, verbose_name='Internal name'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='itemcategory',
|
||||||
|
name='internal_name',
|
||||||
|
field=models.CharField(blank=True,
|
||||||
|
help_text='If you set this, this will be used instead of the public name in the backend.',
|
||||||
|
max_length=255, null=True, verbose_name='Internal name'),
|
||||||
|
),
|
||||||
|
]
|
||||||
20
src/pretix/base/migrations/0091_auto_20180513_1641.py
Normal file
20
src/pretix/base/migrations/0091_auto_20180513_1641.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.11 on 2018-05-13 16:41
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0090_auto_20180509_0917'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='order',
|
||||||
|
name='last_modified',
|
||||||
|
field=models.DateTimeField(auto_now=True, db_index=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
23
src/pretix/base/migrations/0092_auto_20180511_1224.py
Normal file
23
src/pretix/base/migrations/0092_auto_20180511_1224.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.11 on 2018-05-11 12:24
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0091_auto_20180513_1641'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='item',
|
||||||
|
name='original_price',
|
||||||
|
field=models.DecimalField(blank=True, decimal_places=2,
|
||||||
|
help_text='If set, this will be displayed next to the current price to show '
|
||||||
|
'that the current price is a discounted one. This is just a cosmetic '
|
||||||
|
'setting and will not actually impact pricing.',
|
||||||
|
max_digits=7, null=True, verbose_name='Original price'),
|
||||||
|
),
|
||||||
|
]
|
||||||
44
src/pretix/base/migrations/0093_auto_20180528_1432.py
Normal file
44
src/pretix/base/migrations/0093_auto_20180528_1432.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.13 on 2018-05-28 14:32
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.utils.crypto import get_random_string
|
||||||
|
|
||||||
|
|
||||||
|
def set_pids(apps, schema_editor):
|
||||||
|
OrderPosition = apps.get_model('pretixbase', 'OrderPosition') # noqa
|
||||||
|
taken = set()
|
||||||
|
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
|
||||||
|
for op in OrderPosition.objects.iterator():
|
||||||
|
while True:
|
||||||
|
code = get_random_string(length=10, allowed_chars=charset)
|
||||||
|
if code not in taken:
|
||||||
|
op.pseudonymization_id = code
|
||||||
|
taken.add(code)
|
||||||
|
break
|
||||||
|
op.save(update_fields=['pseudonymization_id'])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0092_auto_20180511_1224'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='orderposition',
|
||||||
|
name='pseudonymization_id',
|
||||||
|
field=models.CharField(db_index=True, max_length=16, null=True, unique=True),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
set_pids,
|
||||||
|
migrations.RunPython.noop,
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='orderposition',
|
||||||
|
name='pseudonymization_id',
|
||||||
|
field=models.CharField(db_index=True, default='', max_length=16, unique=True),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
15
src/pretix/base/migrations/0094_auto_20180604_1119.py
Normal file
15
src/pretix/base/migrations/0094_auto_20180604_1119.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.13 on 2018-06-04 11:19
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0093_auto_20180528_1432'),
|
||||||
|
('pretixapi', '0001_initial')
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
]
|
||||||
21
src/pretix/base/migrations/0095_auto_20180604_1129.py
Normal file
21
src/pretix/base/migrations/0095_auto_20180604_1129.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.13 on 2018-06-04 11:29
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0094_auto_20180604_1119'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='logentry',
|
||||||
|
name='oauth_application',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to='pretixapi.OAuthApplication'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -25,13 +25,13 @@ class UserManager(BaseUserManager):
|
|||||||
model documentation to see what's so special about our user model.
|
model documentation to see what's so special about our user model.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def create_user(self, email: str, password: str=None, **kwargs):
|
def create_user(self, email: str, password: str = None, **kwargs):
|
||||||
user = self.model(email=email, **kwargs)
|
user = self.model(email=email, **kwargs)
|
||||||
user.set_password(password)
|
user.set_password(password)
|
||||||
user.save()
|
user.save()
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def create_superuser(self, email: str, password: str=None): # NOQA
|
def create_superuser(self, email: str, password: str = None): # NOQA
|
||||||
# Not used in the software but required by Django
|
# Not used in the software but required by Django
|
||||||
if password is None:
|
if password is None:
|
||||||
raise Exception("You must provide a password")
|
raise Exception("You must provide a password")
|
||||||
@@ -93,7 +93,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
verbose_name=_('Timezone'))
|
verbose_name=_('Timezone'))
|
||||||
require_2fa = models.BooleanField(
|
require_2fa = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
verbose_name=_('Two-factor authentification is required to log in')
|
verbose_name=_('Two-factor authentication is required to log in')
|
||||||
)
|
)
|
||||||
notifications_send = models.BooleanField(
|
notifications_send = models.BooleanField(
|
||||||
default=True,
|
default=True,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ def cached_file_delete(sender, instance, **kwargs):
|
|||||||
|
|
||||||
class LoggingMixin:
|
class LoggingMixin:
|
||||||
|
|
||||||
def log_action(self, action, data=None, user=None, api_token=None, save=True):
|
def log_action(self, action, data=None, user=None, api_token=None, auth=None, save=True):
|
||||||
"""
|
"""
|
||||||
Create a LogEntry object that is related to this object.
|
Create a LogEntry object that is related to this object.
|
||||||
See the LogEntry documentation for details.
|
See the LogEntry documentation for details.
|
||||||
@@ -47,6 +47,8 @@ class LoggingMixin:
|
|||||||
"""
|
"""
|
||||||
from .log import LogEntry
|
from .log import LogEntry
|
||||||
from .event import Event
|
from .event import Event
|
||||||
|
from pretix.api.models import OAuthAccessToken, OAuthApplication
|
||||||
|
from .organizer import TeamAPIToken
|
||||||
from ..notifications import get_all_notification_types
|
from ..notifications import get_all_notification_types
|
||||||
from ..services.notifications import notify
|
from ..services.notifications import notify
|
||||||
|
|
||||||
@@ -57,7 +59,18 @@ class LoggingMixin:
|
|||||||
event = self.event
|
event = self.event
|
||||||
if user and not user.is_authenticated:
|
if user and not user.is_authenticated:
|
||||||
user = None
|
user = None
|
||||||
logentry = LogEntry(content_object=self, user=user, action_type=action, event=event, api_token=api_token)
|
|
||||||
|
kwargs = {}
|
||||||
|
if isinstance(auth, OAuthAccessToken):
|
||||||
|
kwargs['oauth_application'] = auth.application
|
||||||
|
elif isinstance(auth, OAuthApplication):
|
||||||
|
kwargs['oauth_application'] = auth
|
||||||
|
elif isinstance(auth, TeamAPIToken):
|
||||||
|
kwargs['api_token'] = auth
|
||||||
|
elif isinstance(api_token, TeamAPIToken):
|
||||||
|
kwargs['api_token'] = api_token
|
||||||
|
|
||||||
|
logentry = LogEntry(content_object=self, user=user, action_type=action, event=event, **kwargs)
|
||||||
if data:
|
if data:
|
||||||
logentry.data = json.dumps(data, cls=CustomJSONEncoder)
|
logentry.data = json.dumps(data, cls=CustomJSONEncoder)
|
||||||
if save:
|
if save:
|
||||||
@@ -83,4 +96,4 @@ class LoggedModel(models.Model, LoggingMixin):
|
|||||||
|
|
||||||
return LogEntry.objects.filter(
|
return LogEntry.objects.filter(
|
||||||
content_type=ContentType.objects.get_for_model(type(self)), object_id=self.pk
|
content_type=ContentType.objects.get_for_model(type(self)), object_id=self.pk
|
||||||
).select_related('user', 'event')
|
).select_related('user', 'event', 'oauth_application', 'api_token')
|
||||||
|
|||||||
@@ -168,3 +168,11 @@ class Checkin(models.Model):
|
|||||||
return "<Checkin: pos {} on list '{}' at {}>".format(
|
return "<Checkin: pos {} on list '{}' at {}>".format(
|
||||||
self.position, self.list, self.datetime
|
self.position, self.list, self.datetime
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def save(self, **kwargs):
|
||||||
|
self.position.order.touch()
|
||||||
|
super().save(**kwargs)
|
||||||
|
|
||||||
|
def delete(self, **kwargs):
|
||||||
|
self.position.order.touch()
|
||||||
|
super().delete(**kwargs)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import string
|
|||||||
import uuid
|
import uuid
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from datetime import datetime, time
|
from datetime import datetime, time
|
||||||
|
from operator import attrgetter
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -535,6 +536,23 @@ class Event(EventMixin, LoggedModel):
|
|||||||
)
|
)
|
||||||
).order_by('date_from', 'name')
|
).order_by('date_from', 'name')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def subevent_list_subevents(self):
|
||||||
|
ordering = self.settings.get('frontpage_subevent_ordering', default='date_ascending', as_type=str)
|
||||||
|
orderfields = {
|
||||||
|
'date_ascending': ('date_from', 'name'),
|
||||||
|
'date_descending': ('-date_from', 'name'),
|
||||||
|
'name_ascending': ('name', 'date_from'),
|
||||||
|
'name_descending': ('-name', 'date_from'),
|
||||||
|
}[ordering]
|
||||||
|
subevs = self.subevents.filter(
|
||||||
|
Q(active=True) & (
|
||||||
|
Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
|
||||||
|
| Q(date_to__gte=now())
|
||||||
|
)
|
||||||
|
) # order_by doesn't make sense with I18nField
|
||||||
|
return sorted(subevs, key=attrgetter(*orderfields))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def meta_data(self):
|
def meta_data(self):
|
||||||
data = {p.name: p.default for p in self.organizer.meta_properties.all()}
|
data = {p.name: p.default for p in self.organizer.meta_properties.all()}
|
||||||
@@ -545,7 +563,7 @@ class Event(EventMixin, LoggedModel):
|
|||||||
def has_payment_provider(self):
|
def has_payment_provider(self):
|
||||||
result = False
|
result = False
|
||||||
for provider in self.get_payment_providers().values():
|
for provider in self.get_payment_providers().values():
|
||||||
if provider.is_enabled and provider.identifier != 'free':
|
if provider.is_enabled and provider.identifier not in ('free', 'boxoffice'):
|
||||||
result = True
|
result = True
|
||||||
break
|
break
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -43,6 +43,11 @@ class ItemCategory(LoggedModel):
|
|||||||
max_length=255,
|
max_length=255,
|
||||||
verbose_name=_("Category name"),
|
verbose_name=_("Category name"),
|
||||||
)
|
)
|
||||||
|
internal_name = models.CharField(
|
||||||
|
verbose_name=_("Internal name"),
|
||||||
|
help_text=_("If you set this, this will be used instead of the public name in the backend."),
|
||||||
|
blank=True, null=True, max_length=255
|
||||||
|
)
|
||||||
description = I18nTextField(
|
description = I18nTextField(
|
||||||
blank=True, verbose_name=_("Category description")
|
blank=True, verbose_name=_("Category description")
|
||||||
)
|
)
|
||||||
@@ -63,9 +68,10 @@ class ItemCategory(LoggedModel):
|
|||||||
ordering = ('position', 'id')
|
ordering = ('position', 'id')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
name = self.internal_name or self.name
|
||||||
if self.is_addon:
|
if self.is_addon:
|
||||||
return _('{category} (Add-On products)').format(category=str(self.name))
|
return _('{category} (Add-On products)').format(category=str(name))
|
||||||
return str(self.name)
|
return str(name)
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
super().delete(*args, **kwargs)
|
super().delete(*args, **kwargs)
|
||||||
@@ -185,6 +191,8 @@ class Item(LoggedModel):
|
|||||||
:type min_per_order: int
|
:type min_per_order: int
|
||||||
:param checkin_attention: Requires special attention at check-in
|
:param checkin_attention: Requires special attention at check-in
|
||||||
:type checkin_attention: bool
|
:type checkin_attention: bool
|
||||||
|
:param original_price: The item's "original" price. Will not be used for any calculations, will just be shown.
|
||||||
|
:type original_price: decimal.Decimal
|
||||||
"""
|
"""
|
||||||
|
|
||||||
event = models.ForeignKey(
|
event = models.ForeignKey(
|
||||||
@@ -205,6 +213,11 @@ class Item(LoggedModel):
|
|||||||
max_length=255,
|
max_length=255,
|
||||||
verbose_name=_("Item name"),
|
verbose_name=_("Item name"),
|
||||||
)
|
)
|
||||||
|
internal_name = models.CharField(
|
||||||
|
verbose_name=_("Internal name"),
|
||||||
|
help_text=_("If you set this, this will be used instead of the public name in the backend."),
|
||||||
|
blank=True, null=True, max_length=255
|
||||||
|
)
|
||||||
active = models.BooleanField(
|
active = models.BooleanField(
|
||||||
default=True,
|
default=True,
|
||||||
verbose_name=_("Active"),
|
verbose_name=_("Active"),
|
||||||
@@ -300,8 +313,15 @@ class Item(LoggedModel):
|
|||||||
'attention. You can use this for example for student tickets to indicate to the person at '
|
'attention. You can use this for example for student tickets to indicate to the person at '
|
||||||
'check-in that the student ID card still needs to be checked.')
|
'check-in that the student ID card still needs to be checked.')
|
||||||
)
|
)
|
||||||
|
original_price = models.DecimalField(
|
||||||
|
verbose_name=_('Original price'),
|
||||||
|
blank=True, null=True,
|
||||||
|
max_digits=7, decimal_places=2,
|
||||||
|
help_text=_('If set, this will be displayed next to the current price to show that the current price is a '
|
||||||
|
'discounted one. This is just a cosmetic setting and will not actually impact pricing.')
|
||||||
|
)
|
||||||
# !!! Attention: If you add new fields here, also add them to the copying code in
|
# !!! Attention: If you add new fields here, also add them to the copying code in
|
||||||
# pretix/control/views/item.py if applicable.
|
# pretix/control/forms/item.py if applicable.
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Product")
|
verbose_name = _("Product")
|
||||||
@@ -309,7 +329,7 @@ class Item(LoggedModel):
|
|||||||
ordering = ("category__position", "category", "position")
|
ordering = ("category__position", "category", "position")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.name)
|
return str(self.internal_name or self.name)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
@@ -991,7 +1011,7 @@ class Quota(LoggedModel):
|
|||||||
self.cached_availability_number = res[1]
|
self.cached_availability_number = res[1]
|
||||||
self.cached_availability_time = now_dt
|
self.cached_availability_time = now_dt
|
||||||
if self.size is None:
|
if self.size is None:
|
||||||
self.cached_availability_paid_orders = self.count_pending_orders()
|
self.cached_availability_paid_orders = self.count_paid_orders()
|
||||||
self.save(
|
self.save(
|
||||||
update_fields=[
|
update_fields=[
|
||||||
'cached_availability_state', 'cached_availability_number', 'cached_availability_time',
|
'cached_availability_state', 'cached_availability_number', 'cached_availability_time',
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ class LogEntry(models.Model):
|
|||||||
datetime = models.DateTimeField(auto_now_add=True, db_index=True)
|
datetime = models.DateTimeField(auto_now_add=True, db_index=True)
|
||||||
user = models.ForeignKey('User', null=True, blank=True, on_delete=models.PROTECT)
|
user = models.ForeignKey('User', null=True, blank=True, on_delete=models.PROTECT)
|
||||||
api_token = models.ForeignKey('TeamAPIToken', null=True, blank=True, on_delete=models.PROTECT)
|
api_token = models.ForeignKey('TeamAPIToken', null=True, blank=True, on_delete=models.PROTECT)
|
||||||
|
oauth_application = models.ForeignKey('pretixapi.OAuthApplication', null=True, blank=True, on_delete=models.PROTECT)
|
||||||
event = models.ForeignKey('Event', null=True, blank=True, on_delete=models.SET_NULL)
|
event = models.ForeignKey('Event', null=True, blank=True, on_delete=models.SET_NULL)
|
||||||
action_type = models.CharField(max_length=255)
|
action_type = models.CharField(max_length=255)
|
||||||
data = models.TextField(default='{}')
|
data = models.TextField(default='{}')
|
||||||
@@ -65,10 +66,13 @@ class LogEntry(models.Model):
|
|||||||
def display_object(self):
|
def display_object(self):
|
||||||
from . import Order, Voucher, Quota, Item, ItemCategory, Question, Event, TaxRule, SubEvent
|
from . import Order, Voucher, Quota, Item, ItemCategory, Question, Event, TaxRule, SubEvent
|
||||||
|
|
||||||
if self.content_type.model_class() is Event:
|
try:
|
||||||
return ''
|
if self.content_type.model_class() is Event:
|
||||||
|
return ''
|
||||||
|
|
||||||
co = self.content_object
|
co = self.content_object
|
||||||
|
except:
|
||||||
|
return ''
|
||||||
a_map = None
|
a_map = None
|
||||||
a_text = None
|
a_text = None
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import copy
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import string
|
import string
|
||||||
from datetime import datetime, time
|
from datetime import datetime, time, timedelta
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Any, Dict, List, Union
|
from typing import Any, Dict, List, Union
|
||||||
|
|
||||||
@@ -180,6 +180,9 @@ class Order(LoggedModel):
|
|||||||
verbose_name=_("Meta information"),
|
verbose_name=_("Meta information"),
|
||||||
null=True, blank=True
|
null=True, blank=True
|
||||||
)
|
)
|
||||||
|
last_modified = models.DateTimeField(
|
||||||
|
auto_now=True, db_index=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Order")
|
verbose_name = _("Order")
|
||||||
@@ -208,12 +211,48 @@ class Order(LoggedModel):
|
|||||||
def changable(self):
|
def changable(self):
|
||||||
return self.status in (Order.STATUS_PAID, Order.STATUS_PENDING)
|
return self.status in (Order.STATUS_PAID, Order.STATUS_PENDING)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, **kwargs):
|
||||||
|
if 'update_fields' in kwargs and 'last_modified' not in kwargs['update_fields']:
|
||||||
|
kwargs['update_fields'] = list(kwargs['update_fields']) + ['last_modified']
|
||||||
if not self.code:
|
if not self.code:
|
||||||
self.assign_code()
|
self.assign_code()
|
||||||
if not self.datetime:
|
if not self.datetime:
|
||||||
self.datetime = now()
|
self.datetime = now()
|
||||||
super().save(*args, **kwargs)
|
if not self.expires:
|
||||||
|
self.set_expires()
|
||||||
|
super().save(**kwargs)
|
||||||
|
|
||||||
|
def touch(self):
|
||||||
|
self.save(update_fields=['last_modified'])
|
||||||
|
|
||||||
|
def set_expires(self, now_dt=None, subevents=None):
|
||||||
|
now_dt = now_dt or now()
|
||||||
|
tz = pytz.timezone(self.event.settings.timezone)
|
||||||
|
exp_by_date = now_dt.astimezone(tz) + timedelta(days=self.event.settings.get('payment_term_days', as_type=int))
|
||||||
|
exp_by_date = exp_by_date.astimezone(tz).replace(hour=23, minute=59, second=59, microsecond=0)
|
||||||
|
if self.event.settings.get('payment_term_weekdays'):
|
||||||
|
if exp_by_date.weekday() == 5:
|
||||||
|
exp_by_date += timedelta(days=2)
|
||||||
|
elif exp_by_date.weekday() == 6:
|
||||||
|
exp_by_date += timedelta(days=1)
|
||||||
|
|
||||||
|
self.expires = exp_by_date
|
||||||
|
|
||||||
|
term_last = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
|
||||||
|
if term_last:
|
||||||
|
if self.event.has_subevents and subevents:
|
||||||
|
term_last = min([
|
||||||
|
term_last.datetime(se).date()
|
||||||
|
for se in subevents
|
||||||
|
])
|
||||||
|
else:
|
||||||
|
term_last = term_last.datetime(self.event).date()
|
||||||
|
term_last = make_aware(datetime.combine(
|
||||||
|
term_last,
|
||||||
|
time(hour=23, minute=59, second=59)
|
||||||
|
), tz)
|
||||||
|
if term_last < self.expires:
|
||||||
|
self.expires = term_last
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def tax_total(self):
|
def tax_total(self):
|
||||||
@@ -547,8 +586,15 @@ class QuestionAnswer(models.Model):
|
|||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if self.orderposition and self.cartposition:
|
if self.orderposition and self.cartposition:
|
||||||
raise ValueError('QuestionAnswer cannot be linked to an order and a cart position at the same time.')
|
raise ValueError('QuestionAnswer cannot be linked to an order and a cart position at the same time.')
|
||||||
|
if self.orderposition:
|
||||||
|
self.orderposition.order.touch()
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def delete(self, **kwargs):
|
||||||
|
if self.orderposition:
|
||||||
|
self.orderposition.order.touch()
|
||||||
|
super().delete(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
class AbstractPosition(models.Model):
|
class AbstractPosition(models.Model):
|
||||||
"""
|
"""
|
||||||
@@ -751,8 +797,13 @@ class OrderFee(models.Model):
|
|||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if self.tax_rate is None:
|
if self.tax_rate is None:
|
||||||
self._calculate_tax()
|
self._calculate_tax()
|
||||||
|
self.order.touch()
|
||||||
return super().save(*args, **kwargs)
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def delete(self, **kwargs):
|
||||||
|
self.order.touch()
|
||||||
|
super().delete(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
class OrderPosition(AbstractPosition):
|
class OrderPosition(AbstractPosition):
|
||||||
"""
|
"""
|
||||||
@@ -784,6 +835,11 @@ class OrderPosition(AbstractPosition):
|
|||||||
verbose_name=_('Tax value')
|
verbose_name=_('Tax value')
|
||||||
)
|
)
|
||||||
secret = models.CharField(max_length=64, default=generate_position_secret, db_index=True)
|
secret = models.CharField(max_length=64, default=generate_position_secret, db_index=True)
|
||||||
|
pseudonymization_id = models.CharField(
|
||||||
|
max_length=16,
|
||||||
|
unique=True,
|
||||||
|
db_index=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Order position")
|
verbose_name = _("Order position")
|
||||||
@@ -861,11 +917,28 @@ class OrderPosition(AbstractPosition):
|
|||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if self.tax_rate is None:
|
if self.tax_rate is None:
|
||||||
self._calculate_tax()
|
self._calculate_tax()
|
||||||
|
self.order.touch()
|
||||||
if self.pk is None:
|
if self.pk is None:
|
||||||
while OrderPosition.objects.filter(secret=self.secret).exists():
|
while OrderPosition.objects.filter(secret=self.secret).exists():
|
||||||
self.secret = generate_position_secret()
|
self.secret = generate_position_secret()
|
||||||
|
|
||||||
|
if not self.pseudonymization_id:
|
||||||
|
self.assign_pseudonymization_id()
|
||||||
|
|
||||||
return super().save(*args, **kwargs)
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def assign_pseudonymization_id(self):
|
||||||
|
# This omits some character pairs completely because they are hard to read even on screens (1/I and O/0)
|
||||||
|
# and includes only one of two characters for some pairs because they are sometimes hard to distinguish in
|
||||||
|
# handwriting (2/Z, 4/A, 5/S, 6/G). This allows for better detection e.g. in incoming wire transfers that
|
||||||
|
# might include OCR'd handwritten text
|
||||||
|
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
|
||||||
|
while True:
|
||||||
|
code = get_random_string(length=10, allowed_chars=charset)
|
||||||
|
if not OrderPosition.objects.filter(pseudonymization_id=code).exists():
|
||||||
|
self.pseudonymization_id = code
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
class CartPosition(AbstractPosition):
|
class CartPosition(AbstractPosition):
|
||||||
"""
|
"""
|
||||||
@@ -945,6 +1018,11 @@ class InvoiceAddress(models.Model):
|
|||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def save(self, **kwargs):
|
||||||
|
if self.order:
|
||||||
|
self.order.touch()
|
||||||
|
super().save(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
def cachedticket_name(instance, filename: str) -> str:
|
def cachedticket_name(instance, filename: str) -> str:
|
||||||
secret = get_random_string(length=16, allowed_chars=string.ascii_letters + string.digits)
|
secret = get_random_string(length=16, allowed_chars=string.ascii_letters + string.digits)
|
||||||
|
|||||||
@@ -137,7 +137,9 @@ class Team(LoggedModel):
|
|||||||
)
|
)
|
||||||
can_change_organizer_settings = models.BooleanField(
|
can_change_organizer_settings = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
verbose_name=_("Can change organizer settings")
|
verbose_name=_("Can change organizer settings"),
|
||||||
|
help_text=_('Someone with this setting can get access to most data of all of your events, i.e. via privacy '
|
||||||
|
'reports, so be careful who you add to this team!')
|
||||||
)
|
)
|
||||||
|
|
||||||
can_change_event_settings = models.BooleanField(
|
can_change_event_settings = models.BooleanField(
|
||||||
|
|||||||
@@ -160,18 +160,19 @@ class TaxRule(LoggedModel):
|
|||||||
|
|
||||||
def get_matching_rule(self, invoice_address):
|
def get_matching_rule(self, invoice_address):
|
||||||
rules = json.loads(self.custom_rules)
|
rules = json.loads(self.custom_rules)
|
||||||
for r in rules:
|
if invoice_address:
|
||||||
if r['country'] == 'EU' and str(invoice_address.country) not in EU_COUNTRIES:
|
for r in rules:
|
||||||
continue
|
if r['country'] == 'EU' and str(invoice_address.country) not in EU_COUNTRIES:
|
||||||
if r['country'] not in ('ZZ', 'EU') and r['country'] != str(invoice_address.country):
|
continue
|
||||||
continue
|
if r['country'] not in ('ZZ', 'EU') and r['country'] != str(invoice_address.country):
|
||||||
if r['address_type'] == 'individual' and invoice_address.is_business:
|
continue
|
||||||
continue
|
if r['address_type'] == 'individual' and invoice_address.is_business:
|
||||||
if r['address_type'] in ('business', 'business_vat_id') and not invoice_address.is_business:
|
continue
|
||||||
continue
|
if r['address_type'] in ('business', 'business_vat_id') and not invoice_address.is_business:
|
||||||
if r['address_type'] == 'business_vat_id' and (not invoice_address.vat_id or not invoice_address.vat_id_validated):
|
continue
|
||||||
continue
|
if r['address_type'] == 'business_vat_id' and (not invoice_address.vat_id or not invoice_address.vat_id_validated):
|
||||||
return r
|
continue
|
||||||
|
return r
|
||||||
return {'action': 'vat'}
|
return {'action': 'vat'}
|
||||||
|
|
||||||
def is_reverse_charge(self, invoice_address):
|
def is_reverse_charge(self, invoice_address):
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class WaitingListEntry(LoggedModel):
|
|||||||
WaitingListEntry.clean_itemvar(self.event, self.item, self.variation)
|
WaitingListEntry.clean_itemvar(self.event, self.item, self.variation)
|
||||||
WaitingListEntry.clean_subevent(self.event, self.subevent)
|
WaitingListEntry.clean_subevent(self.event, self.subevent)
|
||||||
|
|
||||||
def send_voucher(self, quota_cache=None, user=None, api_token=None):
|
def send_voucher(self, quota_cache=None, user=None, auth=None):
|
||||||
availability = (
|
availability = (
|
||||||
self.variation.check_quotas(count_waitinglist=False, subevent=self.subevent, _cache=quota_cache)
|
self.variation.check_quotas(count_waitinglist=False, subevent=self.subevent, _cache=quota_cache)
|
||||||
if self.variation
|
if self.variation
|
||||||
@@ -114,8 +114,8 @@ class WaitingListEntry(LoggedModel):
|
|||||||
'email': self.email,
|
'email': self.email,
|
||||||
'waitinglistentry': self.pk,
|
'waitinglistentry': self.pk,
|
||||||
'subevent': self.subevent.pk if self.subevent else None,
|
'subevent': self.subevent.pk if self.subevent else None,
|
||||||
}, user=user, api_token=api_token)
|
}, user=user, auth=auth)
|
||||||
self.log_action('pretix.waitinglist.voucher', user=user, api_token=api_token)
|
self.log_action('pretix.waitinglist.voucher', user=user, auth=auth)
|
||||||
self.voucher = v
|
self.voucher = v
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import pytz
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.forms import Form
|
from django.forms import Form
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
@@ -185,6 +186,28 @@ class BasePaymentProvider:
|
|||||||
widget=I18nTextarea,
|
widget=I18nTextarea,
|
||||||
widget_kwargs={'attrs': {'rows': '2'}}
|
widget_kwargs={'attrs': {'rows': '2'}}
|
||||||
)),
|
)),
|
||||||
|
('_total_min',
|
||||||
|
forms.DecimalField(
|
||||||
|
label=_('Minimum order total'),
|
||||||
|
help_text=_('This payment will be available only if the order total is equal to or exceeds the given '
|
||||||
|
'value. The order total for this purpose may be computed without taking the fees imposed '
|
||||||
|
'by this payment method into account.'),
|
||||||
|
localize=True,
|
||||||
|
required=False,
|
||||||
|
decimal_places=places,
|
||||||
|
widget=DecimalTextInput(places=places)
|
||||||
|
)),
|
||||||
|
('_total_max',
|
||||||
|
forms.DecimalField(
|
||||||
|
label=_('Maximum order total'),
|
||||||
|
help_text=_('This payment will be available only if the order total is equal to or below the given '
|
||||||
|
'value. The order total for this purpose may be computed without taking the fees imposed '
|
||||||
|
'by this payment method into account.'),
|
||||||
|
localize=True,
|
||||||
|
required=False,
|
||||||
|
decimal_places=places,
|
||||||
|
widget=DecimalTextInput(places=places)
|
||||||
|
)),
|
||||||
('_fee_abs',
|
('_fee_abs',
|
||||||
forms.DecimalField(
|
forms.DecimalField(
|
||||||
label=_('Additional fee'),
|
label=_('Additional fee'),
|
||||||
@@ -304,16 +327,36 @@ class BasePaymentProvider:
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def is_allowed(self, request: HttpRequest) -> bool:
|
def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool:
|
||||||
"""
|
"""
|
||||||
You can use this method to disable this payment provider for certain groups
|
You can use this method to disable this payment provider for certain groups
|
||||||
of users, products or other criteria. If this method returns ``False``, the
|
of users, products or other criteria. If this method returns ``False``, the
|
||||||
user will not be able to select this payment method. This will only be called
|
user will not be able to select this payment method. This will only be called
|
||||||
during checkout, not on retrying.
|
during checkout, not on retrying.
|
||||||
|
|
||||||
The default implementation checks for the _availability_date setting to be either unset or in the future.
|
The default implementation checks for the _availability_date setting to be either unset or in the future
|
||||||
|
and for the _total_max and _total_min requirements to be met.
|
||||||
|
|
||||||
|
:param total: The total value without the payment method fee, after taxes.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.17.0
|
||||||
|
|
||||||
|
The ``total`` parameter has been added. For backwards compatibility, this method is called again
|
||||||
|
without this parameter if it raises a ``TypeError`` on first try.
|
||||||
"""
|
"""
|
||||||
return self._is_still_available(cart_id=get_or_create_cart_id(request))
|
timing = self._is_still_available(cart_id=get_or_create_cart_id(request))
|
||||||
|
pricing = True
|
||||||
|
|
||||||
|
if (self.settings._total_max is not None or self.settings._total_min is not None) and total is None:
|
||||||
|
raise ImproperlyConfigured('This payment provider does not support maximum or minimum amounts.')
|
||||||
|
|
||||||
|
if self.settings._total_max is not None:
|
||||||
|
pricing = pricing and total <= Decimal(self.settings._total_max)
|
||||||
|
|
||||||
|
if self.settings._total_min is not None:
|
||||||
|
pricing = pricing and total >= Decimal(self.settings._total_min)
|
||||||
|
|
||||||
|
return timing and pricing
|
||||||
|
|
||||||
def payment_form_render(self, request: HttpRequest) -> str:
|
def payment_form_render(self, request: HttpRequest) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -451,6 +494,12 @@ class BasePaymentProvider:
|
|||||||
|
|
||||||
:param order: The order object
|
:param order: The order object
|
||||||
"""
|
"""
|
||||||
|
if self.settings._total_max is not None and order.total > Decimal(self.settings._total_max):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.settings._total_min is not None and order.total < Decimal(self.settings._total_min):
|
||||||
|
return False
|
||||||
|
|
||||||
return self._is_still_available(order=order)
|
return self._is_still_available(order=order)
|
||||||
|
|
||||||
def order_can_retry(self, order: Order) -> bool:
|
def order_can_retry(self, order: Order) -> bool:
|
||||||
@@ -647,7 +696,7 @@ class FreeOrderProvider(BasePaymentProvider):
|
|||||||
mark_order_refunded(order, user=request.user)
|
mark_order_refunded(order, user=request.user)
|
||||||
messages.success(request, _('The order has been marked as refunded.'))
|
messages.success(request, _('The order has been marked as refunded.'))
|
||||||
|
|
||||||
def is_allowed(self, request: HttpRequest) -> bool:
|
def is_allowed(self, request: HttpRequest, total: Decimal) -> bool:
|
||||||
from .services.cart import get_fees
|
from .services.cart import get_fees
|
||||||
|
|
||||||
total = get_cart_total(request)
|
total = get_cart_total(request)
|
||||||
@@ -658,6 +707,39 @@ class FreeOrderProvider(BasePaymentProvider):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class BoxOfficeProvider(BasePaymentProvider):
|
||||||
|
is_implicit = True
|
||||||
|
is_enabled = True
|
||||||
|
identifier = "boxoffice"
|
||||||
|
verbose_name = _("Box office")
|
||||||
|
|
||||||
|
def payment_perform(self, request: HttpRequest, order: Order):
|
||||||
|
from pretix.base.services.orders import mark_order_paid
|
||||||
|
try:
|
||||||
|
mark_order_paid(order, 'boxoffice', send_mail=False)
|
||||||
|
except Quota.QuotaExceededException as e:
|
||||||
|
raise PaymentException(str(e))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def settings_form_fields(self) -> dict:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def order_control_refund_render(self, order: Order) -> str:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def order_control_refund_perform(self, request: HttpRequest, order: Order) -> Union[bool, str]:
|
||||||
|
from pretix.base.services.orders import mark_order_refunded
|
||||||
|
|
||||||
|
mark_order_refunded(order, user=request.user)
|
||||||
|
messages.success(request, _('The order has been marked as refunded.'))
|
||||||
|
|
||||||
|
def is_allowed(self, request: HttpRequest, total: Decimal) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def order_change_allowed(self, order: Order) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
@receiver(register_payment_providers, dispatch_uid="payment_free")
|
@receiver(register_payment_providers, dispatch_uid="payment_free")
|
||||||
def register_payment_provider(sender, **kwargs):
|
def register_payment_provider(sender, **kwargs):
|
||||||
return FreeOrderProvider
|
return [FreeOrderProvider, BoxOfficeProvider]
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ DEFAULT_VARIABLES = OrderedDict((
|
|||||||
("item", {
|
("item", {
|
||||||
"label": _("Product name"),
|
"label": _("Product name"),
|
||||||
"editor_sample": _("Sample product"),
|
"editor_sample": _("Sample product"),
|
||||||
"evaluate": lambda orderposition, order, event: str(orderposition.item)
|
"evaluate": lambda orderposition, order, event: str(orderposition.item.name)
|
||||||
}),
|
}),
|
||||||
("variation", {
|
("variation", {
|
||||||
"label": _("Variation name"),
|
"label": _("Variation name"),
|
||||||
@@ -62,8 +62,8 @@ DEFAULT_VARIABLES = OrderedDict((
|
|||||||
"label": _("Product name and variation"),
|
"label": _("Product name and variation"),
|
||||||
"editor_sample": _("Sample product – sample variation"),
|
"editor_sample": _("Sample product – sample variation"),
|
||||||
"evaluate": lambda orderposition, order, event: (
|
"evaluate": lambda orderposition, order, event: (
|
||||||
'{} - {}'.format(orderposition.item, orderposition.variation)
|
'{} - {}'.format(orderposition.item.name, orderposition.variation)
|
||||||
if orderposition.variation else str(orderposition.item)
|
if orderposition.variation else str(orderposition.item.name)
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
("item_category", {
|
("item_category", {
|
||||||
@@ -148,12 +148,12 @@ DEFAULT_VARIABLES = OrderedDict((
|
|||||||
("invoice_name", {
|
("invoice_name", {
|
||||||
"label": _("Invoice address: name"),
|
"label": _("Invoice address: name"),
|
||||||
"editor_sample": _("John Doe"),
|
"editor_sample": _("John Doe"),
|
||||||
"evaluate": lambda op, order, ev: order.invoice_address.name if getattr(order, 'invoice_address') else ''
|
"evaluate": lambda op, order, ev: order.invoice_address.name if getattr(order, 'invoice_address', None) else ''
|
||||||
}),
|
}),
|
||||||
("invoice_company", {
|
("invoice_company", {
|
||||||
"label": _("Invoice address: company"),
|
"label": _("Invoice address: company"),
|
||||||
"editor_sample": _("Sample company"),
|
"editor_sample": _("Sample company"),
|
||||||
"evaluate": lambda op, order, ev: order.invoice_address.company if getattr(order, 'invoice_address') else ''
|
"evaluate": lambda op, order, ev: order.invoice_address.company if getattr(order, 'invoice_address', None) else ''
|
||||||
}),
|
}),
|
||||||
("addons", {
|
("addons", {
|
||||||
"label": _("List of Add-Ons"),
|
"label": _("List of Add-Ons"),
|
||||||
@@ -211,16 +211,25 @@ class Renderer:
|
|||||||
pdfmetrics.registerFont(TTFont(family + ' B I', finders.find(styles['bolditalic']['truetype'])))
|
pdfmetrics.registerFont(TTFont(family + ' B I', finders.find(styles['bolditalic']['truetype'])))
|
||||||
|
|
||||||
def _draw_barcodearea(self, canvas: Canvas, op: OrderPosition, o: dict):
|
def _draw_barcodearea(self, canvas: Canvas, op: OrderPosition, o: dict):
|
||||||
|
content = o.get('content', 'secret')
|
||||||
|
if content == 'secret':
|
||||||
|
content = op.secret
|
||||||
|
elif content == 'pseudonymization_id':
|
||||||
|
content = op.pseudonymization_id
|
||||||
|
|
||||||
reqs = float(o['size']) * mm
|
reqs = float(o['size']) * mm
|
||||||
qrw = QrCodeWidget(op.secret, barLevel='H', barHeight=reqs, barWidth=reqs)
|
qrw = QrCodeWidget(content, barLevel='H', barHeight=reqs, barWidth=reqs)
|
||||||
d = Drawing(reqs, reqs)
|
d = Drawing(reqs, reqs)
|
||||||
d.add(qrw)
|
d.add(qrw)
|
||||||
qr_x = float(o['left']) * mm
|
qr_x = float(o['left']) * mm
|
||||||
qr_y = float(o['bottom']) * mm
|
qr_y = float(o['bottom']) * mm
|
||||||
renderPDF.draw(d, canvas, qr_x, qr_y)
|
renderPDF.draw(d, canvas, qr_x, qr_y)
|
||||||
|
|
||||||
|
def _get_ev(self, op, order):
|
||||||
|
return op.subevent or order.event
|
||||||
|
|
||||||
def _get_text_content(self, op: OrderPosition, order: Order, o: dict):
|
def _get_text_content(self, op: OrderPosition, order: Order, o: dict):
|
||||||
ev = op.subevent or order.event
|
ev = self._get_ev(op, order)
|
||||||
if not o['content']:
|
if not o['content']:
|
||||||
return '(error)'
|
return '(error)'
|
||||||
if o['content'] == 'other':
|
if o['content'] == 'other':
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from django.dispatch import receiver
|
|||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import pgettext_lazy, ugettext as _
|
from django.utils.translation import pgettext_lazy, ugettext as _
|
||||||
|
|
||||||
from pretix.base.i18n import LazyLocaleException, language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CartPosition, Event, InvoiceAddress, Item, ItemVariation, Voucher,
|
CartPosition, Event, InvoiceAddress, Item, ItemVariation, Voucher,
|
||||||
)
|
)
|
||||||
@@ -27,8 +27,16 @@ from pretix.presale.signals import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CartError(LazyLocaleException):
|
class CartError(Exception):
|
||||||
pass
|
def __init__(self, *args):
|
||||||
|
msg = args[0]
|
||||||
|
msgargs = args[1] if len(args) > 1 else None
|
||||||
|
self.args = args
|
||||||
|
if msgargs:
|
||||||
|
msg = _(msg) % msgargs
|
||||||
|
else:
|
||||||
|
msg = _(msg)
|
||||||
|
super().__init__(msg)
|
||||||
|
|
||||||
|
|
||||||
error_messages = {
|
error_messages = {
|
||||||
|
|||||||
@@ -39,7 +39,10 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
|||||||
introductory = invoice.event.settings.get('invoice_introductory_text', as_type=LazyI18nString)
|
introductory = invoice.event.settings.get('invoice_introductory_text', as_type=LazyI18nString)
|
||||||
additional = invoice.event.settings.get('invoice_additional_text', as_type=LazyI18nString)
|
additional = invoice.event.settings.get('invoice_additional_text', as_type=LazyI18nString)
|
||||||
footer = invoice.event.settings.get('invoice_footer_text', as_type=LazyI18nString)
|
footer = invoice.event.settings.get('invoice_footer_text', as_type=LazyI18nString)
|
||||||
payment = payment_provider.render_invoice_text(invoice.order)
|
if payment_provider:
|
||||||
|
payment = payment_provider.render_invoice_text(invoice.order)
|
||||||
|
else:
|
||||||
|
payment = ""
|
||||||
|
|
||||||
invoice.introductory_text = str(introductory).replace('\n', '<br />')
|
invoice.introductory_text = str(introductory).replace('\n', '<br />')
|
||||||
invoice.additional_text = str(additional).replace('\n', '<br />')
|
invoice.additional_text = str(additional).replace('\n', '<br />')
|
||||||
@@ -171,6 +174,7 @@ def generate_cancellation(invoice: Invoice, trigger_pdf=True):
|
|||||||
cancellation.is_cancellation = True
|
cancellation.is_cancellation = True
|
||||||
cancellation.date = timezone.now().date()
|
cancellation.date = timezone.now().date()
|
||||||
cancellation.payment_provider_text = ''
|
cancellation.payment_provider_text = ''
|
||||||
|
cancellation.file = None
|
||||||
cancellation.save()
|
cancellation.save()
|
||||||
|
|
||||||
cancellation = build_cancellation(cancellation)
|
cancellation = build_cancellation(cancellation)
|
||||||
|
|||||||
@@ -106,14 +106,19 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
|
|||||||
'color': '#8E44B3'
|
'color': '#8E44B3'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bcc = []
|
||||||
if event:
|
if event:
|
||||||
htmlctx['event'] = event
|
htmlctx['event'] = event
|
||||||
htmlctx['color'] = event.settings.primary_color
|
htmlctx['color'] = event.settings.primary_color
|
||||||
|
if event.settings.mail_bcc:
|
||||||
|
bcc.append(event.settings.mail_bcc)
|
||||||
|
|
||||||
if event.settings.mail_from == settings.DEFAULT_FROM_EMAIL and event.settings.contact_mail and not headers.get('Reply-To'):
|
if event.settings.mail_from == settings.DEFAULT_FROM_EMAIL and event.settings.contact_mail and not headers.get('Reply-To'):
|
||||||
headers['Reply-To'] = event.settings.contact_mail
|
headers['Reply-To'] = event.settings.contact_mail
|
||||||
|
|
||||||
prefix = event.settings.get('mail_prefix')
|
prefix = event.settings.get('mail_prefix')
|
||||||
|
if prefix and prefix.startswith('[') and prefix.endswith(']'):
|
||||||
|
prefix = prefix[1:-1]
|
||||||
if prefix:
|
if prefix:
|
||||||
subject = "[%s] %s" % (prefix, subject)
|
subject = "[%s] %s" % (prefix, subject)
|
||||||
|
|
||||||
@@ -151,6 +156,7 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
|
|||||||
|
|
||||||
send_task = mail_send_task.si(
|
send_task = mail_send_task.si(
|
||||||
to=[email],
|
to=[email],
|
||||||
|
bcc=bcc,
|
||||||
subject=subject,
|
subject=subject,
|
||||||
body=body_plain,
|
body=body_plain,
|
||||||
html=body_html,
|
html=body_html,
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ from django.db import transaction
|
|||||||
from django.db.models import F, Max, Q, Sum
|
from django.db.models import F, Max, Q, Sum
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.timezone import make_aware, now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
|
from pretix.api.models import OAuthApplication
|
||||||
from pretix.base.i18n import (
|
from pretix.base.i18n import (
|
||||||
LazyCurrencyNumber, LazyDate, LazyLocaleException, LazyNumber, language,
|
LazyCurrencyNumber, LazyDate, LazyLocaleException, LazyNumber, language,
|
||||||
)
|
)
|
||||||
@@ -31,7 +32,6 @@ from pretix.base.models.orders import (
|
|||||||
from pretix.base.models.organizer import TeamAPIToken
|
from pretix.base.models.organizer import TeamAPIToken
|
||||||
from pretix.base.models.tax import TaxedPrice
|
from pretix.base.models.tax import TaxedPrice
|
||||||
from pretix.base.payment import BasePaymentProvider
|
from pretix.base.payment import BasePaymentProvider
|
||||||
from pretix.base.reldate import RelativeDateWrapper
|
|
||||||
from pretix.base.services.async import ProfiledTask
|
from pretix.base.services.async import ProfiledTask
|
||||||
from pretix.base.services.invoices import (
|
from pretix.base.services.invoices import (
|
||||||
generate_cancellation, generate_invoice, invoice_qualified,
|
generate_cancellation, generate_invoice, invoice_qualified,
|
||||||
@@ -81,7 +81,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
def mark_order_paid(order: Order, provider: str=None, info: str=None, date: datetime=None, manual: bool=None,
|
def mark_order_paid(order: Order, provider: str=None, info: str=None, date: datetime=None, manual: bool=None,
|
||||||
force: bool=False, send_mail: bool=True, user: User=None, mail_text='',
|
force: bool=False, send_mail: bool=True, user: User=None, mail_text='',
|
||||||
count_waitinglist=True, api_token=None) -> Order:
|
count_waitinglist=True, auth=None) -> Order:
|
||||||
"""
|
"""
|
||||||
Marks an order as paid. This sets the payment provider, info and date and returns
|
Marks an order as paid. This sets the payment provider, info and date and returns
|
||||||
the order object.
|
the order object.
|
||||||
@@ -124,7 +124,7 @@ def mark_order_paid(order: Order, provider: str=None, info: str=None, date: date
|
|||||||
'date': date or now_dt,
|
'date': date or now_dt,
|
||||||
'manual': manual,
|
'manual': manual,
|
||||||
'force': force
|
'force': force
|
||||||
}, user=user, api_token=api_token)
|
}, user=user, auth=auth)
|
||||||
order_paid.send(order.event, order=order)
|
order_paid.send(order.event, order=order)
|
||||||
|
|
||||||
invoice = None
|
invoice = None
|
||||||
@@ -174,7 +174,7 @@ def mark_order_paid(order: Order, provider: str=None, info: str=None, date: date
|
|||||||
return order
|
return order
|
||||||
|
|
||||||
|
|
||||||
def extend_order(order: Order, new_date: datetime, force: bool=False, user: User=None, api_token=None):
|
def extend_order(order: Order, new_date: datetime, force: bool=False, user: User=None, auth=None):
|
||||||
"""
|
"""
|
||||||
Extends the deadline of an order. If the order is already expired, the quota will be checked to
|
Extends the deadline of an order. If the order is already expired, the quota will be checked to
|
||||||
see if this is actually still possible. If ``force`` is set to ``True``, the result of this check
|
see if this is actually still possible. If ``force`` is set to ``True``, the result of this check
|
||||||
@@ -188,7 +188,7 @@ def extend_order(order: Order, new_date: datetime, force: bool=False, user: User
|
|||||||
order.log_action(
|
order.log_action(
|
||||||
'pretix.event.order.expirychanged',
|
'pretix.event.order.expirychanged',
|
||||||
user=user,
|
user=user,
|
||||||
api_token=api_token,
|
auth=auth,
|
||||||
data={
|
data={
|
||||||
'expires': order.expires,
|
'expires': order.expires,
|
||||||
'state_change': False
|
'state_change': False
|
||||||
@@ -204,7 +204,7 @@ def extend_order(order: Order, new_date: datetime, force: bool=False, user: User
|
|||||||
order.log_action(
|
order.log_action(
|
||||||
'pretix.event.order.expirychanged',
|
'pretix.event.order.expirychanged',
|
||||||
user=user,
|
user=user,
|
||||||
api_token=api_token,
|
auth=auth,
|
||||||
data={
|
data={
|
||||||
'expires': order.expires,
|
'expires': order.expires,
|
||||||
'state_change': True
|
'state_change': True
|
||||||
@@ -215,7 +215,7 @@ def extend_order(order: Order, new_date: datetime, force: bool=False, user: User
|
|||||||
|
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def mark_order_refunded(order, user=None):
|
def mark_order_refunded(order, user=None, api_token=None):
|
||||||
"""
|
"""
|
||||||
Mark this order as refunded. This sets the payment status and returns the order object.
|
Mark this order as refunded. This sets the payment status and returns the order object.
|
||||||
:param order: The order to change
|
:param order: The order to change
|
||||||
@@ -229,7 +229,7 @@ def mark_order_refunded(order, user=None):
|
|||||||
order.status = Order.STATUS_REFUNDED
|
order.status = Order.STATUS_REFUNDED
|
||||||
order.save()
|
order.save()
|
||||||
|
|
||||||
order.log_action('pretix.event.order.refunded', user=user)
|
order.log_action('pretix.event.order.refunded', user=user, api_token=api_token)
|
||||||
i = order.invoices.filter(is_cancellation=False).last()
|
i = order.invoices.filter(is_cancellation=False).last()
|
||||||
if i:
|
if i:
|
||||||
generate_cancellation(i)
|
generate_cancellation(i)
|
||||||
@@ -238,24 +238,21 @@ def mark_order_refunded(order, user=None):
|
|||||||
|
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def mark_order_expired(order, user=None, api_token=None):
|
def mark_order_expired(order, user=None, auth=None):
|
||||||
"""
|
"""
|
||||||
Mark this order as expired. This sets the payment status and returns the order object.
|
Mark this order as expired. This sets the payment status and returns the order object.
|
||||||
:param order: The order to change
|
:param order: The order to change
|
||||||
:param user: The user that performed the change
|
:param user: The user that performed the change
|
||||||
:param api_token: The API token used to performed the change
|
|
||||||
"""
|
"""
|
||||||
if isinstance(order, int):
|
if isinstance(order, int):
|
||||||
order = Order.objects.get(pk=order)
|
order = Order.objects.get(pk=order)
|
||||||
if isinstance(user, int):
|
if isinstance(user, int):
|
||||||
user = User.objects.get(pk=user)
|
user = User.objects.get(pk=user)
|
||||||
if isinstance(api_token, int):
|
|
||||||
api_token = TeamAPIToken.objects.get(pk=api_token)
|
|
||||||
with order.event.lock():
|
with order.event.lock():
|
||||||
order.status = Order.STATUS_EXPIRED
|
order.status = Order.STATUS_EXPIRED
|
||||||
order.save()
|
order.save()
|
||||||
|
|
||||||
order.log_action('pretix.event.order.expired', user=user, api_token=api_token)
|
order.log_action('pretix.event.order.expired', user=user, auth=auth)
|
||||||
i = order.invoices.filter(is_cancellation=False).last()
|
i = order.invoices.filter(is_cancellation=False).last()
|
||||||
if i:
|
if i:
|
||||||
generate_cancellation(i)
|
generate_cancellation(i)
|
||||||
@@ -264,7 +261,7 @@ def mark_order_expired(order, user=None, api_token=None):
|
|||||||
|
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def _cancel_order(order, user=None, send_mail: bool=True, api_token=None):
|
def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, oauth_application=None):
|
||||||
"""
|
"""
|
||||||
Mark this order as canceled
|
Mark this order as canceled
|
||||||
:param order: The order to change
|
:param order: The order to change
|
||||||
@@ -276,13 +273,15 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None):
|
|||||||
user = User.objects.get(pk=user)
|
user = User.objects.get(pk=user)
|
||||||
if isinstance(api_token, int):
|
if isinstance(api_token, int):
|
||||||
api_token = TeamAPIToken.objects.get(pk=api_token)
|
api_token = TeamAPIToken.objects.get(pk=api_token)
|
||||||
|
if isinstance(oauth_application, int):
|
||||||
|
oauth_application = OAuthApplication.objects.get(pk=oauth_application)
|
||||||
with order.event.lock():
|
with order.event.lock():
|
||||||
if not order.cancel_allowed():
|
if not order.cancel_allowed():
|
||||||
raise OrderError(_('You cannot cancel this order.'))
|
raise OrderError(_('You cannot cancel this order.'))
|
||||||
order.status = Order.STATUS_CANCELED
|
order.status = Order.STATUS_CANCELED
|
||||||
order.save()
|
order.save()
|
||||||
|
|
||||||
order.log_action('pretix.event.order.canceled', user=user, api_token=api_token)
|
order.log_action('pretix.event.order.canceled', user=user, auth=api_token or oauth_application)
|
||||||
i = order.invoices.filter(is_cancellation=False).last()
|
i = order.invoices.filter(is_cancellation=False).last()
|
||||||
if i:
|
if i:
|
||||||
generate_cancellation(i)
|
generate_cancellation(i)
|
||||||
@@ -301,8 +300,8 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None):
|
|||||||
'secret': order.secret
|
'secret': order.secret
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
email_subject = _('Order canceled: %(code)s') % {'code': order.code}
|
|
||||||
with language(order.locale):
|
with language(order.locale):
|
||||||
|
email_subject = _('Order canceled: %(code)s') % {'code': order.code}
|
||||||
try:
|
try:
|
||||||
order.send_mail(
|
order.send_mail(
|
||||||
email_subject, email_template, email_context,
|
email_subject, email_template, email_context,
|
||||||
@@ -448,50 +447,22 @@ def _get_fees(positions: List[CartPosition], payment_provider: BasePaymentProvid
|
|||||||
def _create_order(event: Event, email: str, positions: List[CartPosition], now_dt: datetime,
|
def _create_order(event: Event, email: str, positions: List[CartPosition], now_dt: datetime,
|
||||||
payment_provider: BasePaymentProvider, locale: str=None, address: InvoiceAddress=None,
|
payment_provider: BasePaymentProvider, locale: str=None, address: InvoiceAddress=None,
|
||||||
meta_info: dict=None):
|
meta_info: dict=None):
|
||||||
from datetime import time
|
|
||||||
|
|
||||||
fees = _get_fees(positions, payment_provider, address, meta_info, event)
|
fees = _get_fees(positions, payment_provider, address, meta_info, event)
|
||||||
total = sum([c.price for c in positions]) + sum([c.value for c in fees])
|
total = sum([c.price for c in positions]) + sum([c.value for c in fees])
|
||||||
|
|
||||||
tz = pytz.timezone(event.settings.timezone)
|
|
||||||
exp_by_date = now_dt.astimezone(tz) + timedelta(days=event.settings.get('payment_term_days', as_type=int))
|
|
||||||
exp_by_date = exp_by_date.astimezone(tz).replace(hour=23, minute=59, second=59, microsecond=0)
|
|
||||||
if event.settings.get('payment_term_weekdays'):
|
|
||||||
if exp_by_date.weekday() == 5:
|
|
||||||
exp_by_date += timedelta(days=2)
|
|
||||||
elif exp_by_date.weekday() == 6:
|
|
||||||
exp_by_date += timedelta(days=1)
|
|
||||||
|
|
||||||
expires = exp_by_date
|
|
||||||
|
|
||||||
term_last = event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
|
|
||||||
if term_last:
|
|
||||||
if event.has_subevents:
|
|
||||||
term_last = min([
|
|
||||||
term_last.datetime(se).date()
|
|
||||||
for se in event.subevents.filter(id__in=[p.subevent_id for p in positions])
|
|
||||||
])
|
|
||||||
else:
|
|
||||||
term_last = term_last.datetime(event).date()
|
|
||||||
term_last = make_aware(datetime.combine(
|
|
||||||
term_last,
|
|
||||||
time(hour=23, minute=59, second=59)
|
|
||||||
), tz)
|
|
||||||
if term_last < expires:
|
|
||||||
expires = term_last
|
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
order = Order.objects.create(
|
order = Order(
|
||||||
status=Order.STATUS_PENDING,
|
status=Order.STATUS_PENDING,
|
||||||
event=event,
|
event=event,
|
||||||
email=email,
|
email=email,
|
||||||
datetime=now_dt,
|
datetime=now_dt,
|
||||||
expires=expires,
|
|
||||||
locale=locale,
|
locale=locale,
|
||||||
total=total,
|
total=total,
|
||||||
payment_provider=payment_provider.identifier,
|
payment_provider=payment_provider.identifier,
|
||||||
meta_info=json.dumps(meta_info or {}),
|
meta_info=json.dumps(meta_info or {}),
|
||||||
)
|
)
|
||||||
|
order.set_expires(now_dt, event.subevents.filter(id__in=[p.subevent_id for p in positions]))
|
||||||
|
order.save()
|
||||||
|
|
||||||
if address:
|
if address:
|
||||||
if address.order is not None:
|
if address.order is not None:
|
||||||
@@ -510,6 +481,9 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
|
|||||||
|
|
||||||
OrderPosition.transform_cart_positions(positions, order)
|
OrderPosition.transform_cart_positions(positions, order)
|
||||||
order.log_action('pretix.event.order.placed')
|
order.log_action('pretix.event.order.placed')
|
||||||
|
if meta_info:
|
||||||
|
for msg in meta_info.get('confirm_messages', []):
|
||||||
|
order.log_action('pretix.event.order.consent', data={'msg': msg})
|
||||||
|
|
||||||
order_placed.send(event, order=order)
|
order_placed.send(event, order=order)
|
||||||
return order
|
return order
|
||||||
@@ -672,24 +646,25 @@ def send_download_reminders(sender, **kwargs):
|
|||||||
if not all([r for rr, r in allow_ticket_download.send(e, order=o)]):
|
if not all([r for rr, r in allow_ticket_download.send(e, order=o)]):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
o.download_reminder_sent = True
|
with language(o.locale):
|
||||||
o.save()
|
o.download_reminder_sent = True
|
||||||
email_template = e.settings.mail_text_download_reminder
|
o.save()
|
||||||
email_context = {
|
email_template = e.settings.mail_text_download_reminder
|
||||||
'event': o.event.name,
|
email_context = {
|
||||||
'url': build_absolute_uri(o.event, 'presale:event.order', kwargs={
|
'event': o.event.name,
|
||||||
'order': o.code,
|
'url': build_absolute_uri(o.event, 'presale:event.order', kwargs={
|
||||||
'secret': o.secret
|
'order': o.code,
|
||||||
}),
|
'secret': o.secret
|
||||||
}
|
}),
|
||||||
email_subject = _('Your ticket is ready for download: %(code)s') % {'code': o.code}
|
}
|
||||||
try:
|
email_subject = _('Your ticket is ready for download: %(code)s') % {'code': o.code}
|
||||||
o.send_mail(
|
try:
|
||||||
email_subject, email_template, email_context,
|
o.send_mail(
|
||||||
'pretix.event.order.email.download_reminder_sent'
|
email_subject, email_template, email_context,
|
||||||
)
|
'pretix.event.order.email.download_reminder_sent'
|
||||||
except SendMailException:
|
)
|
||||||
logger.exception('Reminder email could not be sent')
|
except SendMailException:
|
||||||
|
logger.exception('Reminder email could not be sent')
|
||||||
|
|
||||||
|
|
||||||
class OrderChangeManager:
|
class OrderChangeManager:
|
||||||
@@ -705,7 +680,7 @@ class OrderChangeManager:
|
|||||||
'no quota is available.'),
|
'no quota is available.'),
|
||||||
'paid_price_change': _('Currently, paid orders can only be changed in a way that does not change the total '
|
'paid_price_change': _('Currently, paid orders can only be changed in a way that does not change the total '
|
||||||
'price of the order as partial payments or refunds are not yet supported.'),
|
'price of the order as partial payments or refunds are not yet supported.'),
|
||||||
'addon_to_required': _('This is an addon product, please select the base position it should be added to.'),
|
'addon_to_required': _('This is an add-on product, please select the base position it should be added to.'),
|
||||||
'addon_invalid': _('The selected base position does not allow you to add this product as an add-on.'),
|
'addon_invalid': _('The selected base position does not allow you to add this product as an add-on.'),
|
||||||
'subevent_required': _('You need to choose a subevent for the new position.'),
|
'subevent_required': _('You need to choose a subevent for the new position.'),
|
||||||
}
|
}
|
||||||
@@ -715,6 +690,7 @@ class OrderChangeManager:
|
|||||||
CancelOperation = namedtuple('CancelOperation', ('position',))
|
CancelOperation = namedtuple('CancelOperation', ('position',))
|
||||||
AddOperation = namedtuple('AddOperation', ('item', 'variation', 'price', 'addon_to', 'subevent'))
|
AddOperation = namedtuple('AddOperation', ('item', 'variation', 'price', 'addon_to', 'subevent'))
|
||||||
SplitOperation = namedtuple('SplitOperation', ('position',))
|
SplitOperation = namedtuple('SplitOperation', ('position',))
|
||||||
|
RegenerateSecretOperation = namedtuple('RegenerateSecretOperation', ('position',))
|
||||||
|
|
||||||
def __init__(self, order: Order, user, notify=True):
|
def __init__(self, order: Order, user, notify=True):
|
||||||
self.order = order
|
self.order = order
|
||||||
@@ -770,6 +746,9 @@ class OrderChangeManager:
|
|||||||
self._quotadiff.subtract(position.quotas)
|
self._quotadiff.subtract(position.quotas)
|
||||||
self._operations.append(self.SubeventOperation(position, subevent, price))
|
self._operations.append(self.SubeventOperation(position, subevent, price))
|
||||||
|
|
||||||
|
def regenerate_secret(self, position: OrderPosition):
|
||||||
|
self._operations.append(self.RegenerateSecretOperation(position))
|
||||||
|
|
||||||
def change_price(self, position: OrderPosition, price: Decimal):
|
def change_price(self, position: OrderPosition, price: Decimal):
|
||||||
price = position.item.tax(price)
|
price = position.item.tax(price)
|
||||||
|
|
||||||
@@ -976,6 +955,15 @@ class OrderChangeManager:
|
|||||||
})
|
})
|
||||||
elif isinstance(op, self.SplitOperation):
|
elif isinstance(op, self.SplitOperation):
|
||||||
split_positions.append(op.position)
|
split_positions.append(op.position)
|
||||||
|
elif isinstance(op, self.RegenerateSecretOperation):
|
||||||
|
op.position.secret = generate_position_secret()
|
||||||
|
op.position.save()
|
||||||
|
CachedTicket.objects.filter(order_position__order=self.order).delete()
|
||||||
|
CachedCombinedTicket.objects.filter(order=self.order).delete()
|
||||||
|
self.order.log_action('pretix.event.order.changed.secret', user=self.user, data={
|
||||||
|
'position': op.position.pk,
|
||||||
|
'positionid': op.position.positionid,
|
||||||
|
})
|
||||||
|
|
||||||
if split_positions:
|
if split_positions:
|
||||||
self.split_order = self._create_split_order(split_positions)
|
self.split_order = self._create_split_order(split_positions)
|
||||||
@@ -1140,6 +1128,7 @@ class OrderChangeManager:
|
|||||||
self._recalculate_total_and_payment_fee()
|
self._recalculate_total_and_payment_fee()
|
||||||
self._reissue_invoice()
|
self._reissue_invoice()
|
||||||
self._clear_tickets_cache()
|
self._clear_tickets_cache()
|
||||||
|
self.order.touch()
|
||||||
self._check_paid_to_free()
|
self._check_paid_to_free()
|
||||||
|
|
||||||
if self.notify:
|
if self.notify:
|
||||||
@@ -1175,10 +1164,10 @@ def perform_order(self, event: str, payment_provider: str, positions: List[str],
|
|||||||
|
|
||||||
|
|
||||||
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
|
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
|
||||||
def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None):
|
def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None, oauth_application=None):
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
return _cancel_order(order, user, send_mail, api_token)
|
return _cancel_order(order, user, send_mail, api_token, oauth_application)
|
||||||
except LockTimeoutException:
|
except LockTimeoutException:
|
||||||
self.retry()
|
self.retry()
|
||||||
except (MaxRetriesExceededError, LockTimeoutException):
|
except (MaxRetriesExceededError, LockTimeoutException):
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ from pretix.base.signals import order_fee_type_name
|
|||||||
|
|
||||||
|
|
||||||
class DummyObject:
|
class DummyObject:
|
||||||
pass
|
def __str__(self):
|
||||||
|
return str(self.name)
|
||||||
|
|
||||||
|
|
||||||
class Dontsum:
|
class Dontsum:
|
||||||
|
|||||||
@@ -225,6 +225,10 @@ DEFAULTS = {
|
|||||||
'default': None,
|
'default': None,
|
||||||
'type': str
|
'type': str
|
||||||
},
|
},
|
||||||
|
'mail_bcc': {
|
||||||
|
'default': None,
|
||||||
|
'type': str
|
||||||
|
},
|
||||||
'mail_from': {
|
'mail_from': {
|
||||||
'default': settings.MAIL_FROM,
|
'default': settings.MAIL_FROM,
|
||||||
'type': str
|
'type': str
|
||||||
@@ -495,6 +499,10 @@ Your {event} team"""))
|
|||||||
'default': '',
|
'default': '',
|
||||||
'type': LazyI18nString
|
'type': LazyI18nString
|
||||||
},
|
},
|
||||||
|
'frontpage_subevent_ordering': {
|
||||||
|
'default': 'date_ascending',
|
||||||
|
'type': str
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
settings_hierarkey = Hierarkey(attribute_name='settings')
|
settings_hierarkey = Hierarkey(attribute_name='settings')
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ def shred_constraints(event: Event):
|
|||||||
max_to=Max('date_to'),
|
max_to=Max('date_to'),
|
||||||
max_fromto=Greatest(Max('date_to'), Max('date_from'))
|
max_fromto=Greatest(Max('date_to'), Max('date_from'))
|
||||||
)
|
)
|
||||||
max_date = max_date['max_fromto'] or max_date['max_to'] or max_date['max_From']
|
max_date = max_date['max_fromto'] or max_date['max_to'] or max_date['max_from']
|
||||||
if max_date > now() - timedelta(days=60):
|
if max_date > now() - timedelta(days=60):
|
||||||
return _('Your event needs to be over for at least 60 days to use this feature.')
|
return _('Your event needs to be over for at least 60 days to use this feature.')
|
||||||
else:
|
else:
|
||||||
@@ -74,6 +74,13 @@ class BaseDataShredder:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError() # NOQA
|
raise NotImplementedError() # NOQA
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tax_relevant(self):
|
||||||
|
"""
|
||||||
|
Indicates whether this removes potentially tax-relevant data.
|
||||||
|
"""
|
||||||
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def verbose_name(self) -> str:
|
def verbose_name(self) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -216,6 +223,7 @@ class AttendeeNameShredder(BaseDataShredder):
|
|||||||
class InvoiceAddressShredder(BaseDataShredder):
|
class InvoiceAddressShredder(BaseDataShredder):
|
||||||
verbose_name = _('Invoice addresses')
|
verbose_name = _('Invoice addresses')
|
||||||
identifier = 'invoice_addresses'
|
identifier = 'invoice_addresses'
|
||||||
|
tax_relevant = True
|
||||||
description = _('This will remove all invoice addresses from orders, as well as logged changes to them.')
|
description = _('This will remove all invoice addresses from orders, as well as logged changes to them.')
|
||||||
|
|
||||||
def generate_files(self) -> List[Tuple[str, str, str]]:
|
def generate_files(self) -> List[Tuple[str, str, str]]:
|
||||||
@@ -269,6 +277,7 @@ class QuestionAnswerShredder(BaseDataShredder):
|
|||||||
class InvoiceShredder(BaseDataShredder):
|
class InvoiceShredder(BaseDataShredder):
|
||||||
verbose_name = _('Invoices')
|
verbose_name = _('Invoices')
|
||||||
identifier = 'invoices'
|
identifier = 'invoices'
|
||||||
|
tax_relevant = True
|
||||||
description = _('This will remove all invoice PDFs, as well as any of their text content that might contain '
|
description = _('This will remove all invoice PDFs, as well as any of their text content that might contain '
|
||||||
'personal data from the database. Invoice numbers and totals will be conserved.')
|
'personal data from the database. Invoice numbers and totals will be conserved.')
|
||||||
|
|
||||||
@@ -312,6 +321,7 @@ class CachedTicketShredder(BaseDataShredder):
|
|||||||
class PaymentInfoShredder(BaseDataShredder):
|
class PaymentInfoShredder(BaseDataShredder):
|
||||||
verbose_name = _('Payment information')
|
verbose_name = _('Payment information')
|
||||||
identifier = 'payment_info'
|
identifier = 'payment_info'
|
||||||
|
tax_relevant = True
|
||||||
description = _('This will remove payment-related information. Depending on the payment method, all data will be '
|
description = _('This will remove payment-related information. Depending on the payment method, all data will be '
|
||||||
'removed or personal data only. No download will be offered.')
|
'removed or personal data only. No download will be offered.')
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from django.http import FileResponse, Http404, HttpRequest, HttpResponse
|
from django.http import Http404, HttpRequest, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
|
|
||||||
from pretix.base.models import CachedFile
|
from pretix.base.models import CachedFile
|
||||||
|
from pretix.helpers.http import ChunkBasedFileResponse
|
||||||
|
|
||||||
|
|
||||||
class DownloadView(TemplateView):
|
class DownloadView(TemplateView):
|
||||||
@@ -20,7 +21,7 @@ class DownloadView(TemplateView):
|
|||||||
if 'ajax' in request.GET:
|
if 'ajax' in request.GET:
|
||||||
return HttpResponse('1' if self.object.file else '0')
|
return HttpResponse('1' if self.object.file else '0')
|
||||||
elif self.object.file:
|
elif self.object.file:
|
||||||
resp = FileResponse(self.object.file.file, content_type=self.object.type)
|
resp = ChunkBasedFileResponse(self.object.file.file, content_type=self.object.type)
|
||||||
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(self.object.filename)
|
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(self.object.filename)
|
||||||
return resp
|
return resp
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from django.db.models import Prefetch
|
|||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
from pretix.base.forms.questions import (
|
from pretix.base.forms.questions import (
|
||||||
BaseInvoiceAddressForm, BaseQuestionsForm,
|
BaseInvoiceAddressForm, BaseInvoiceNameForm, BaseQuestionsForm,
|
||||||
)
|
)
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CartPosition, InvoiceAddress, OrderPosition, Question, QuestionAnswer,
|
CartPosition, InvoiceAddress, OrderPosition, Question, QuestionAnswer,
|
||||||
@@ -144,6 +144,7 @@ class BaseQuestionsViewMixin:
|
|||||||
|
|
||||||
class OrderQuestionsViewMixin(BaseQuestionsViewMixin):
|
class OrderQuestionsViewMixin(BaseQuestionsViewMixin):
|
||||||
invoice_form_class = BaseInvoiceAddressForm
|
invoice_form_class = BaseInvoiceAddressForm
|
||||||
|
invoice_name_form_class = BaseInvoiceNameForm
|
||||||
only_user_visible = True
|
only_user_visible = True
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
@@ -184,6 +185,12 @@ class OrderQuestionsViewMixin(BaseQuestionsViewMixin):
|
|||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def invoice_form(self):
|
def invoice_form(self):
|
||||||
|
if not self.request.event.settings.invoice_address_asked and self.request.event.settings.invoice_name_required:
|
||||||
|
return self.invoice_name_form_class(
|
||||||
|
data=self.request.POST if self.request.method == "POST" else None,
|
||||||
|
event=self.request.event,
|
||||||
|
instance=self.invoice_address, validate_vat_id=False
|
||||||
|
)
|
||||||
return self.invoice_form_class(
|
return self.invoice_form_class(
|
||||||
data=self.request.POST if self.request.method == "POST" else None,
|
data=self.request.POST if self.request.method == "POST" else None,
|
||||||
event=self.request.event,
|
event=self.request.event,
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ def redir_view(request):
|
|||||||
url = signer.unsign(request.GET.get('url', ''))
|
url = signer.unsign(request.GET.get('url', ''))
|
||||||
except signing.BadSignature:
|
except signing.BadSignature:
|
||||||
return HttpResponseBadRequest('Invalid parameter')
|
return HttpResponseBadRequest('Invalid parameter')
|
||||||
return HttpResponseRedirect(url)
|
r = HttpResponseRedirect(url)
|
||||||
|
r['X-Robots-Tag'] = 'noindex'
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
def safelink(url):
|
def safelink(url):
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ from django.core.validators import RegexValidator
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.forms import formset_factory
|
from django.forms import formset_factory
|
||||||
from django.utils.timezone import get_current_timezone_name
|
from django.utils.timezone import get_current_timezone_name
|
||||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
from django.utils.translation import (
|
||||||
|
pgettext, pgettext_lazy, ugettext_lazy as _,
|
||||||
|
)
|
||||||
from django_countries import Countries
|
from django_countries import Countries
|
||||||
from django_countries.fields import LazyTypedChoiceField
|
from django_countries.fields import LazyTypedChoiceField
|
||||||
from i18nfield.forms import (
|
from i18nfield.forms import (
|
||||||
@@ -430,8 +432,8 @@ class PaymentSettingsForm(SettingsForm):
|
|||||||
)
|
)
|
||||||
payment_term_weekdays = forms.BooleanField(
|
payment_term_weekdays = forms.BooleanField(
|
||||||
label=_('Only end payment terms on weekdays'),
|
label=_('Only end payment terms on weekdays'),
|
||||||
help_text=_("If this is activated and the payment term of any order ends on a saturday or sunday, it will be "
|
help_text=_("If this is activated and the payment term of any order ends on a Saturday or Sunday, it will be "
|
||||||
"moved to the next monday instead. This is required in some countries by civil law. This will "
|
"moved to the next Monday instead. This is required in some countries by civil law. This will "
|
||||||
"not effect the last date of payments configured above."),
|
"not effect the last date of payments configured above."),
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
@@ -520,8 +522,7 @@ class InvoiceSettingsForm(SettingsForm):
|
|||||||
label=_("Require customer name"),
|
label=_("Require customer name"),
|
||||||
required=False,
|
required=False,
|
||||||
widget=forms.CheckboxInput(
|
widget=forms.CheckboxInput(
|
||||||
attrs={'data-checkbox-dependency': '#id_invoice_address_asked',
|
attrs={'data-inverse-dependency': '#id_invoice_address_required'}
|
||||||
'data-inverse-dependency': '#id_invoice_address_required'}
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
invoice_address_vatid = forms.BooleanField(
|
invoice_address_vatid = forms.BooleanField(
|
||||||
@@ -662,6 +663,11 @@ class MailSettingsForm(SettingsForm):
|
|||||||
label=_("Sender address"),
|
label=_("Sender address"),
|
||||||
help_text=_("Sender address for outgoing emails")
|
help_text=_("Sender address for outgoing emails")
|
||||||
)
|
)
|
||||||
|
mail_bcc = forms.EmailField(
|
||||||
|
label=_("Bcc address"),
|
||||||
|
help_text=_("All emails will be sent to this address as a Bcc copy"),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
mail_text_signature = I18nFormField(
|
mail_text_signature = I18nFormField(
|
||||||
label=_("Signature"),
|
label=_("Signature"),
|
||||||
@@ -855,12 +861,24 @@ class DisplaySettingsForm(SettingsForm):
|
|||||||
label=_("Show variations of a product expanded by default"),
|
label=_("Show variations of a product expanded by default"),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
frontpage_subevent_ordering = forms.ChoiceField(
|
||||||
|
label=pgettext('subevent', 'Date ordering'),
|
||||||
|
choices=[
|
||||||
|
('date_ascending', _('Event start time')),
|
||||||
|
('date_descending', _('Event start time (descending)')),
|
||||||
|
('name_ascending', _('Name')),
|
||||||
|
('name_descending', _('Name (descending)')),
|
||||||
|
], # When adding a new ordering, remember to also define it in the event model
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
event = kwargs['obj']
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['primary_font'].choices += [
|
self.fields['primary_font'].choices += [
|
||||||
(a, a) for a in get_fonts()
|
(a, a) for a in get_fonts()
|
||||||
]
|
]
|
||||||
|
if not event.has_subevents:
|
||||||
|
del self.fields['frontpage_subevent_ordering']
|
||||||
|
|
||||||
|
|
||||||
class TicketSettingsForm(SettingsForm):
|
class TicketSettingsForm(SettingsForm):
|
||||||
@@ -985,6 +1003,7 @@ class WidgetCodeForm(forms.Form):
|
|||||||
)
|
)
|
||||||
compatibility_mode = forms.BooleanField(
|
compatibility_mode = forms.BooleanField(
|
||||||
label=_("Compatibility mode"),
|
label=_("Compatibility mode"),
|
||||||
|
required=False,
|
||||||
help_text=_("Our regular widget doesn't work in all website builders. If you run into trouble, try using "
|
help_text=_("Our regular widget doesn't work in all website builders. If you run into trouble, try using "
|
||||||
"this compatibility mode.")
|
"this compatibility mode.")
|
||||||
)
|
)
|
||||||
@@ -1018,7 +1037,7 @@ class EventDeleteForm(forms.Form):
|
|||||||
}
|
}
|
||||||
user_pw = forms.CharField(
|
user_pw = forms.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
label=_("New password"),
|
label=_("Your password"),
|
||||||
widget=forms.PasswordInput()
|
widget=forms.PasswordInput()
|
||||||
)
|
)
|
||||||
slug = forms.CharField(
|
slug = forms.CharField(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.db.models import Exists, F, OuterRef, Q
|
from django.db.models import Exists, F, OuterRef, Q
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce, ExtractWeekDay
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||||
@@ -130,6 +130,7 @@ class OrderFilterForm(FilterForm):
|
|||||||
matching_positions = OrderPosition.objects.filter(
|
matching_positions = OrderPosition.objects.filter(
|
||||||
Q(order=OuterRef('pk')) & Q(
|
Q(order=OuterRef('pk')) & Q(
|
||||||
Q(attendee_name__icontains=u) | Q(attendee_email__icontains=u)
|
Q(attendee_name__icontains=u) | Q(attendee_email__icontains=u)
|
||||||
|
| Q(secret__istartswith=u)
|
||||||
)
|
)
|
||||||
).values('id')
|
).values('id')
|
||||||
|
|
||||||
@@ -307,6 +308,20 @@ class SubEventFilterForm(FilterForm):
|
|||||||
),
|
),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
weekday = forms.ChoiceField(
|
||||||
|
label=_('Weekday'),
|
||||||
|
choices=(
|
||||||
|
('', _('All days')),
|
||||||
|
('2', _('Monday')),
|
||||||
|
('3', _('Tuesday')),
|
||||||
|
('4', _('Wednesday')),
|
||||||
|
('5', _('Thursday')),
|
||||||
|
('6', _('Friday')),
|
||||||
|
('7', _('Saturday')),
|
||||||
|
('1', _('Sunday')),
|
||||||
|
),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
query = forms.CharField(
|
query = forms.CharField(
|
||||||
label=_('Event name'),
|
label=_('Event name'),
|
||||||
widget=forms.TextInput(attrs={
|
widget=forms.TextInput(attrs={
|
||||||
@@ -336,6 +351,9 @@ class SubEventFilterForm(FilterForm):
|
|||||||
elif fdata.get('status') == 'past':
|
elif fdata.get('status') == 'past':
|
||||||
qs = qs.filter(presale_end__lte=now())
|
qs = qs.filter(presale_end__lte=now())
|
||||||
|
|
||||||
|
if fdata.get('weekday'):
|
||||||
|
qs = qs.annotate(wday=ExtractWeekDay('date_from')).filter(wday=fdata.get('weekday'))
|
||||||
|
|
||||||
if fdata.get('query'):
|
if fdata.get('query'):
|
||||||
query = fdata.get('query')
|
query = fdata.get('query')
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
@@ -548,6 +566,7 @@ class CheckInFilterForm(FilterForm):
|
|||||||
u = fdata.get('user')
|
u = fdata.get('user')
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
Q(order__code__istartswith=u)
|
Q(order__code__istartswith=u)
|
||||||
|
| Q(secret__istartswith=u)
|
||||||
| Q(order__email__icontains=u)
|
| Q(order__email__icontains=u)
|
||||||
| Q(attendee_name__icontains=u)
|
| Q(attendee_name__icontains=u)
|
||||||
| Q(attendee_email__icontains=u)
|
| Q(attendee_email__icontains=u)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class CategoryForm(I18nModelForm):
|
|||||||
localized_fields = '__all__'
|
localized_fields = '__all__'
|
||||||
fields = [
|
fields = [
|
||||||
'name',
|
'name',
|
||||||
|
'internal_name',
|
||||||
'description',
|
'description',
|
||||||
'is_addon'
|
'is_addon'
|
||||||
]
|
]
|
||||||
@@ -90,9 +91,9 @@ class QuotaForm(I18nModelForm):
|
|||||||
for item in items:
|
for item in items:
|
||||||
if len(item.variations.all()) > 0:
|
if len(item.variations.all()) > 0:
|
||||||
for v in item.variations.all():
|
for v in item.variations.all():
|
||||||
choices.append(('{}-{}'.format(item.pk, v.pk), '{} – {}'.format(item.name, v.value)))
|
choices.append(('{}-{}'.format(item.pk, v.pk), '{} – {}'.format(item, v.value)))
|
||||||
else:
|
else:
|
||||||
choices.append(('{}'.format(item.pk), item.name))
|
choices.append(('{}'.format(item.pk), str(item)))
|
||||||
|
|
||||||
self.fields['itemvars'] = forms.MultipleChoiceField(
|
self.fields['itemvars'] = forms.MultipleChoiceField(
|
||||||
label=_('Products'),
|
label=_('Products'),
|
||||||
@@ -225,6 +226,7 @@ class ItemCreateForm(I18nModelForm):
|
|||||||
self.instance.max_per_order = self.cleaned_data['copy_from'].max_per_order
|
self.instance.max_per_order = self.cleaned_data['copy_from'].max_per_order
|
||||||
self.instance.checkin_attention = self.cleaned_data['copy_from'].checkin_attention
|
self.instance.checkin_attention = self.cleaned_data['copy_from'].checkin_attention
|
||||||
self.instance.free_price = self.cleaned_data['copy_from'].free_price
|
self.instance.free_price = self.cleaned_data['copy_from'].free_price
|
||||||
|
self.instance.original_price = self.cleaned_data['copy_from'].original_price
|
||||||
|
|
||||||
self.instance.position = (self.event.items.aggregate(p=Max('position'))['p'] or 0) + 1
|
self.instance.position = (self.event.items.aggregate(p=Max('position'))['p'] or 0) + 1
|
||||||
instance = super().save(*args, **kwargs)
|
instance = super().save(*args, **kwargs)
|
||||||
@@ -282,6 +284,7 @@ class ItemCreateForm(I18nModelForm):
|
|||||||
localized_fields = '__all__'
|
localized_fields = '__all__'
|
||||||
fields = [
|
fields = [
|
||||||
'name',
|
'name',
|
||||||
|
'internal_name',
|
||||||
'category',
|
'category',
|
||||||
'admission',
|
'admission',
|
||||||
'default_price',
|
'default_price',
|
||||||
@@ -308,6 +311,7 @@ class ItemUpdateForm(I18nModelForm):
|
|||||||
fields = [
|
fields = [
|
||||||
'category',
|
'category',
|
||||||
'name',
|
'name',
|
||||||
|
'internal_name',
|
||||||
'active',
|
'active',
|
||||||
'admission',
|
'admission',
|
||||||
'description',
|
'description',
|
||||||
@@ -322,7 +326,8 @@ class ItemUpdateForm(I18nModelForm):
|
|||||||
'allow_cancel',
|
'allow_cancel',
|
||||||
'max_per_order',
|
'max_per_order',
|
||||||
'min_per_order',
|
'min_per_order',
|
||||||
'checkin_attention'
|
'checkin_attention',
|
||||||
|
'original_price'
|
||||||
]
|
]
|
||||||
field_classes = {
|
field_classes = {
|
||||||
'available_from': forms.SplitDateTimeField,
|
'available_from': forms.SplitDateTimeField,
|
||||||
@@ -343,7 +348,7 @@ class ItemVariationsFormSet(I18nFormSet):
|
|||||||
f.fields['DELETE'].disabled = True
|
f.fields['DELETE'].disabled = True
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
message=_('The variation "%s" cannot be deleted because it has already been ordered by a user or '
|
message=_('The variation "%s" cannot be deleted because it has already been ordered by a user or '
|
||||||
'currently is in a users\'s cart. Please set the variation as "inactive" instead.'),
|
'currently is in a user\'s cart. Please set the variation as "inactive" instead.'),
|
||||||
params=(str(f.instance),)
|
params=(str(f.instance),)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ class ExtendForm(I18nModelForm):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
if self.instance.status == Order.STATUS_PENDING or self.instance._is_still_available(now(), count_waitinglist=False) is True:
|
if self.instance.status == Order.STATUS_PENDING or self.instance._is_still_available(now(),
|
||||||
|
count_waitinglist=False)\
|
||||||
|
is True:
|
||||||
del self.fields['quota_ignore']
|
del self.fields['quota_ignore']
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
@@ -47,8 +49,33 @@ class ExtendForm(I18nModelForm):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
class ExporterForm(forms.Form):
|
class MarkPaidForm(forms.Form):
|
||||||
|
force = forms.BooleanField(
|
||||||
|
label=_('Overbook quota and ignore late payment'),
|
||||||
|
help_text=_('If you check this box, this operation will be performed even if it leads to an overbooked quota '
|
||||||
|
'and you having sold more tickets than you planned! The operation will also be performed '
|
||||||
|
'regardless of the settings for late payments.'),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.instance = kwargs.pop("instance")
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
quota_success = (
|
||||||
|
self.instance.status == Order.STATUS_PENDING or
|
||||||
|
self.instance._is_still_available(now(), count_waitinglist=False) is True
|
||||||
|
)
|
||||||
|
term_last = self.instance.payment_term_last
|
||||||
|
term_success = (
|
||||||
|
(not term_last or term_last >= now()) and
|
||||||
|
(self.instance.status == Order.STATUS_PENDING or self.instance.event.settings.get(
|
||||||
|
'payment_term_accept_late'))
|
||||||
|
)
|
||||||
|
if quota_success and term_success:
|
||||||
|
del self.fields['force']
|
||||||
|
|
||||||
|
|
||||||
|
class ExporterForm(forms.Form):
|
||||||
def clean(self):
|
def clean(self):
|
||||||
data = super().clean()
|
data = super().clean()
|
||||||
|
|
||||||
@@ -144,7 +171,7 @@ class OrderPositionAddForm(forms.Form):
|
|||||||
|
|
||||||
choices = []
|
choices = []
|
||||||
for i in order.event.items.prefetch_related('variations').all():
|
for i in order.event.items.prefetch_related('variations').all():
|
||||||
pname = str(i.name)
|
pname = str(i)
|
||||||
if not i.is_available():
|
if not i.is_available():
|
||||||
pname += ' ({})'.format(_('inactive'))
|
pname += ' ({})'.format(_('inactive'))
|
||||||
variations = list(i.variations.all())
|
variations = list(i.variations.all())
|
||||||
@@ -206,6 +233,7 @@ class OrderPositionChangeForm(forms.Form):
|
|||||||
('subevent', 'Change event date'),
|
('subevent', 'Change event date'),
|
||||||
('cancel', 'Remove product'),
|
('cancel', 'Remove product'),
|
||||||
('split', 'Split into new order'),
|
('split', 'Split into new order'),
|
||||||
|
('secret', 'Regenerate secret'),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -243,7 +271,7 @@ class OrderPositionChangeForm(forms.Form):
|
|||||||
|
|
||||||
choices = []
|
choices = []
|
||||||
for i in instance.order.event.items.prefetch_related('variations').all():
|
for i in instance.order.event.items.prefetch_related('variations').all():
|
||||||
pname = str(i.name)
|
pname = str(i)
|
||||||
if not i.is_available():
|
if not i.is_available():
|
||||||
pname += ' ({})'.format(_('inactive'))
|
pname += ' ({})'.format(_('inactive'))
|
||||||
variations = list(i.variations.all())
|
variations = list(i.variations.all())
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ class SubEventItemForm(SubEventItemOrVariationFormMixin, forms.ModelForm):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['price'].widget.attrs['placeholder'] = money_filter(self.item.default_price, self.item.event.currency, hide_currency=True)
|
self.fields['price'].widget.attrs['placeholder'] = money_filter(self.item.default_price, self.item.event.currency, hide_currency=True)
|
||||||
self.fields['price'].label = str(self.item.name)
|
self.fields['price'].label = str(self.item)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SubEventItem
|
model = SubEventItem
|
||||||
@@ -116,7 +116,7 @@ class SubEventItemVariationForm(SubEventItemOrVariationFormMixin, forms.ModelFor
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['price'].widget.attrs['placeholder'] = money_filter(self.variation.price, self.item.event.currency, hide_currency=True)
|
self.fields['price'].widget.attrs['placeholder'] = money_filter(self.variation.price, self.item.event.currency, hide_currency=True)
|
||||||
self.fields['price'].label = '{} – {}'.format(str(self.item.name), self.variation.value)
|
self.fields['price'].label = '{} – {}'.format(str(self.item), self.variation.value)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SubEventItem
|
model = SubEventItem
|
||||||
|
|||||||
@@ -86,13 +86,13 @@ class VoucherForm(I18nModelForm):
|
|||||||
itemid, varid = iv.split('-')
|
itemid, varid = iv.split('-')
|
||||||
i = self.instance.event.items.get(pk=itemid)
|
i = self.instance.event.items.get(pk=itemid)
|
||||||
v = i.variations.get(pk=varid)
|
v = i.variations.get(pk=varid)
|
||||||
choices.append(('%d-%d' % (i.pk, v.pk), '%s – %s' % (i.name, v.value)))
|
choices.append(('%d-%d' % (i.pk, v.pk), '%s – %s' % (str(i), v.value)))
|
||||||
elif iv:
|
elif iv:
|
||||||
i = self.instance.event.items.get(pk=iv)
|
i = self.instance.event.items.get(pk=iv)
|
||||||
if i.variations.exists():
|
if i.variations.exists():
|
||||||
choices.append((str(i.pk), _('{product} – Any variation').format(product=i.name)))
|
choices.append((str(i.pk), _('{product} – Any variation').format(product=i)))
|
||||||
else:
|
else:
|
||||||
choices.append((str(i.pk), str(i.name)))
|
choices.append((str(i.pk), str(i)))
|
||||||
|
|
||||||
self.fields['itemvar'].choices = choices
|
self.fields['itemvar'].choices = choices
|
||||||
self.fields['itemvar'].widget = Select2ItemVarQuota(
|
self.fields['itemvar'].widget = Select2ItemVarQuota(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import bleach
|
||||||
import dateutil.parser
|
import dateutil.parser
|
||||||
import pytz
|
import pytz
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
@@ -81,6 +82,10 @@ def _display_order_changed(event: Event, logentry: LogEntry):
|
|||||||
item=item,
|
item=item,
|
||||||
price=money_filter(Decimal(data['price']), event.currency),
|
price=money_filter(Decimal(data['price']), event.currency),
|
||||||
)
|
)
|
||||||
|
elif logentry.action_type == 'pretix.event.order.changed.secret':
|
||||||
|
return text + ' ' + _('A new secret has been generated for position #{posid}.').format(
|
||||||
|
posid=data.get('positionid', '?'),
|
||||||
|
)
|
||||||
elif logentry.action_type == 'pretix.event.order.changed.split':
|
elif logentry.action_type == 'pretix.event.order.changed.split':
|
||||||
old_item = str(event.items.get(pk=data['old_item']))
|
old_item = str(event.items.get(pk=data['old_item']))
|
||||||
if data['old_variation']:
|
if data['old_variation']:
|
||||||
@@ -192,6 +197,8 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
|||||||
'pretix.user.settings.notifications.enabled': _('Notifications have been enabled.'),
|
'pretix.user.settings.notifications.enabled': _('Notifications have been enabled.'),
|
||||||
'pretix.user.settings.notifications.disabled': _('Notifications have been disabled.'),
|
'pretix.user.settings.notifications.disabled': _('Notifications have been disabled.'),
|
||||||
'pretix.user.settings.notifications.changed': _('Your notification settings have been changed.'),
|
'pretix.user.settings.notifications.changed': _('Your notification settings have been changed.'),
|
||||||
|
'pretix.user.oauth.authorized': _('The application "{application_name}" has been authorized to access your '
|
||||||
|
'account.'),
|
||||||
'pretix.control.auth.user.forgot_password.mail_sent': _('Password reset mail sent.'),
|
'pretix.control.auth.user.forgot_password.mail_sent': _('Password reset mail sent.'),
|
||||||
'pretix.control.auth.user.forgot_password.recovered': _('The password has been reset.'),
|
'pretix.control.auth.user.forgot_password.recovered': _('The password has been reset.'),
|
||||||
'pretix.voucher.added': _('The voucher has been created.'),
|
'pretix.voucher.added': _('The voucher has been created.'),
|
||||||
@@ -278,6 +285,11 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
|||||||
if logentry.action_type.startswith('pretix.event.tickets.provider.'):
|
if logentry.action_type.startswith('pretix.event.tickets.provider.'):
|
||||||
return _('The settings of a ticket output provider have been changed.')
|
return _('The settings of a ticket output provider have been changed.')
|
||||||
|
|
||||||
|
if logentry.action_type == 'pretix.event.order.consent':
|
||||||
|
return _('The user confirmed the following message: "{}"').format(
|
||||||
|
bleach.clean(logentry.parsed_data.get('msg'), tags=[], strip=True)
|
||||||
|
)
|
||||||
|
|
||||||
if logentry.action_type == 'pretix.event.checkin':
|
if logentry.action_type == 'pretix.event.checkin':
|
||||||
return _display_checkin(sender, logentry)
|
return _display_checkin(sender, logentry)
|
||||||
|
|
||||||
|
|||||||
@@ -232,3 +232,10 @@ styles. It is advisable to set a prefix for your form to avoid clashes with othe
|
|||||||
|
|
||||||
As with all plugin signals, the ``sender`` keyword argument will contain the event.
|
As with all plugin signals, the ``sender`` keyword argument will contain the event.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
oauth_application_registered = Signal(
|
||||||
|
providing_args=["user", "application"]
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
This signal will be called whenever a user registers a new OAuth application.
|
||||||
|
"""
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<h3>{% trans "Welcome back!" %}</h3>
|
<h3>{% trans "Welcome back!" %}</h3>
|
||||||
<p>
|
<p>
|
||||||
{% trans "You configured your account to require authentification with a second medium, e.g. your phone. Please enter your verification code here:" %}
|
{% trans "You configured your account to require authentication with a second medium, e.g. your phone. Please enter your verification code here:" %}
|
||||||
</p>
|
</p>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input class="form-control" name="token" placeholder="{% trans "Token" %}"
|
<input class="form-control" name="token" placeholder="{% trans "Token" %}"
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
{% extends "pretixcontrol/auth/base.html" %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block content %}
|
||||||
|
{% if not error %}
|
||||||
|
<form class="form-signin" action="" method="post">
|
||||||
|
<h3>{% trans "Authorize an application" %}</h3>
|
||||||
|
|
||||||
|
{% csrf_token %}
|
||||||
|
{% for field in form %}
|
||||||
|
{% if field.is_hidden %}
|
||||||
|
{{ field }}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{% blocktrans trimmed with application=application.name %}
|
||||||
|
Do you really want to grant the application <strong>{{ application }}</strong> access to your
|
||||||
|
pretix account?
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
<p>{% trans "The application requires the following permissions:" %}</p>
|
||||||
|
<ul>
|
||||||
|
{% for scope in scopes_descriptions %}
|
||||||
|
<li>{{ scope }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<p>{% trans "Please select the organizer accounts this application should get access to:" %}</p>
|
||||||
|
{% bootstrap_field form.organizers layout="inline" %}
|
||||||
|
|
||||||
|
{% bootstrap_form_errors form layout="control" %}
|
||||||
|
<p class="text-danger">
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
This application has <strong>not</strong> been reviewed by the pretix team. Granting access to your
|
||||||
|
pretix account happens at your own risk.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="form-group buttons">
|
||||||
|
<input type="submit" class="btn btn-large btn-default" value="Cancel"/>
|
||||||
|
<input type="submit" class="btn btn-large btn-primary" name="allow" value="Authorize"/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<form class="form-signin" action="" method="post">
|
||||||
|
<h3>{% trans "Error:" %} {{ error.error }}</h3>
|
||||||
|
<p>{{ error.description }}</p>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
@@ -42,6 +42,7 @@
|
|||||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/mail.js" %}"></script>
|
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/mail.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/typeahead.js" %}"></script>
|
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/typeahead.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/quicksetup.js" %}"></script>
|
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/quicksetup.js" %}"></script>
|
||||||
|
<script type="text/javascript" src="{% static "pretixbase/js/details.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "pretixbase/js/asynctask.js" %}"></script>
|
<script type="text/javascript" src="{% static "pretixbase/js/asynctask.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "pretixbase/js/asyncdownload.js" %}"></script>
|
<script type="text/javascript" src="{% static "pretixbase/js/asyncdownload.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "colorpicker/bootstrap-colorpicker.js" %}"></script>
|
<script type="text/javascript" src="{% static "colorpicker/bootstrap-colorpicker.js" %}"></script>
|
||||||
@@ -53,7 +54,10 @@
|
|||||||
<link rel="icon" href="{% static "pretixbase/img/favicon.ico" %}">
|
<link rel="icon" href="{% static "pretixbase/img/favicon.ico" %}">
|
||||||
{% block custom_header %}{% endblock %}
|
{% block custom_header %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body data-datetimeformat="{{ js_datetime_format }}" data-timeformat="{{ js_time_format }}" data-dateformat="{{ js_date_format }}" data-datetimelocale="{{ js_locale }}" data-payment-weekdays-disabled="{{ js_payment_weekdays_disabled }}" data-select2-locale="{{ select2locale }}" data-longdateformat="{{ js_long_date_format }}">
|
<body data-datetimeformat="{{ js_datetime_format }}" data-timeformat="{{ js_time_format }}"
|
||||||
|
data-dateformat="{{ js_date_format }}" data-datetimelocale="{{ js_locale }}"
|
||||||
|
data-payment-weekdays-disabled="{{ js_payment_weekdays_disabled }}"
|
||||||
|
data-select2-locale="{{ select2locale }}" data-longdateformat="{{ js_long_date_format }}" class="nojs">
|
||||||
<div id="wrapper">
|
<div id="wrapper">
|
||||||
<nav class="navbar navbar-inverse navbar-static-top" role="navigation">
|
<nav class="navbar navbar-inverse navbar-static-top" role="navigation">
|
||||||
<div class="navbar-header">
|
<div class="navbar-header">
|
||||||
|
|||||||
@@ -90,7 +90,7 @@
|
|||||||
<span class="label label-warning">{% trans "unpaid" %}</span>
|
<span class="label label-warning">{% trans "unpaid" %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ e.item.name }}{% if e.variation %} – {{ e.variation }}{% endif %}</td>
|
<td>{{ e.item }}{% if e.variation %} – {{ e.variation }}{% endif %}</td>
|
||||||
<td>{{ e.order.email }}</td>
|
<td>{{ e.order.email }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if e.addon_to %}
|
{% if e.addon_to %}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{% load i18n %}{% blocktrans with url=url|safe %}Hello,
|
{% load i18n %}{% blocktrans with url=url|safe messages=messages|safe %}Hello,
|
||||||
|
|
||||||
this is to inform you that the account information of your pretix account has been
|
this is to inform you that the account information of your pretix account has been
|
||||||
changed. In particular, the following changes have been performed:
|
changed. In particular, the following changes have been performed:
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
<ul class="nav nav-second-level">
|
<ul class="nav nav-second-level">
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url 'control:event.items' organizer=request.event.organizer.slug event=request.event.slug %}"
|
<a href="{% url 'control:event.items' organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||||
{% if "event.items" == url_name or "event.item." in url_name or url_name == "event.item" %}class="active"{% endif %}>
|
{% if "event.items" == url_name or "event.item." in url_name or "event.items.add" == url_name or url_name == "event.item" %}class="active"{% endif %}>
|
||||||
{% trans "Products" %}</a>
|
{% trans "Products" %}</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
|||||||
@@ -11,6 +11,9 @@
|
|||||||
{% bootstrap_field form.logo_image layout="control" %}
|
{% bootstrap_field form.logo_image layout="control" %}
|
||||||
{% bootstrap_field form.frontpage_text layout="control" %}
|
{% bootstrap_field form.frontpage_text layout="control" %}
|
||||||
{% bootstrap_field form.show_variations_expanded layout="control" %}
|
{% bootstrap_field form.show_variations_expanded layout="control" %}
|
||||||
|
{% if form.frontpage_subevent_ordering %}
|
||||||
|
{% bootstrap_field form.frontpage_subevent_ordering layout="control" %}
|
||||||
|
{% endif %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "Shop design" %}</legend>
|
<legend>{% trans "Shop design" %}</legend>
|
||||||
|
|||||||
@@ -124,6 +124,13 @@
|
|||||||
<span class="fa fa-user fa-fw"></span>
|
<span class="fa fa-user fa-fw"></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ log.user.get_full_name }}
|
{{ log.user.get_full_name }}
|
||||||
|
{% if log.oauth_application %}
|
||||||
|
<br><span class="fa fa-plug fa-fw"></span>
|
||||||
|
{{ log.oauth_application.name }}
|
||||||
|
{% endif %}
|
||||||
|
{% elif log.api_token %}
|
||||||
|
<span class="fa fa-key fa-fw"></span>
|
||||||
|
{{ log.api_token.name }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-2 col-sm-12 col-xs-12">
|
<div class="col-lg-2 col-sm-12 col-xs-12">
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
</option>
|
</option>
|
||||||
{% for up in userlist %}
|
{% for up in userlist %}
|
||||||
{% if up.user__id %}
|
{% if up.user__id %}
|
||||||
<option value="{{ up.user__id }}" {% if request.GET.user == up.user__id %}selected="selected"{% endif %}>
|
<option value="{{ up.user__id }}"
|
||||||
|
{% if request.GET.user == up.user__id %}selected="selected"{% endif %}>
|
||||||
{{ up.user__email }}
|
{{ up.user__email }}
|
||||||
</option>
|
</option>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -42,13 +43,20 @@
|
|||||||
{% if log.user %}
|
{% if log.user %}
|
||||||
{% if log.user.is_staff %}
|
{% if log.user.is_staff %}
|
||||||
<span class="fa fa-id-card fa-danger fa-fw"
|
<span class="fa fa-id-card fa-danger fa-fw"
|
||||||
data-toggle="tooltip"
|
data-toggle="tooltip"
|
||||||
title="{% trans "This change was performed by a pretix administrator." %}">
|
title="{% trans "This change was performed by a pretix administrator." %}">
|
||||||
</span>
|
</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="fa fa-user fa-fw"></span>
|
<span class="fa fa-user fa-fw"></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ log.user.get_full_name }}
|
{{ log.user.get_full_name }}
|
||||||
|
{% if log.oauth_application %}
|
||||||
|
<br><span class="fa fa-plug fa-fw"></span>
|
||||||
|
{{ log.oauth_application.name }}
|
||||||
|
{% endif %}
|
||||||
|
{% elif log.api_token %}
|
||||||
|
<span class="fa fa-key fa-fw"></span>
|
||||||
|
{{ log.api_token.name }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-2 col-sm-12 col-xs-12">
|
<div class="col-lg-2 col-sm-12 col-xs-12">
|
||||||
@@ -61,7 +69,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<div class="list-group-item">
|
<div class="list-group-item">
|
||||||
<em>{% trans "No results" %}</em>
|
<em>{% trans "No results" %}</em>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
{% bootstrap_field form.mail_prefix layout="control" %}
|
{% bootstrap_field form.mail_prefix layout="control" %}
|
||||||
{% bootstrap_field form.mail_from layout="control" %}
|
{% bootstrap_field form.mail_from layout="control" %}
|
||||||
{% bootstrap_field form.mail_text_signature layout="control" %}
|
{% bootstrap_field form.mail_text_signature layout="control" %}
|
||||||
|
{% bootstrap_field form.mail_bcc layout="control" %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "E-mail content" %}</legend>
|
<legend>{% trans "E-mail content" %}</legend>
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
{% load mail_settings_preview %}
|
{% load mail_settings_preview %}
|
||||||
<div class="panel panel-default">
|
<details class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<summary class="panel-heading">
|
||||||
<h4 class="panel-title">
|
<h4 class="panel-title">
|
||||||
<a class="collapsed" data-toggle="collapse" href="#{{ pid }}">
|
<strong>{% trans title %}</strong>
|
||||||
<strong>{% trans title %}</strong>
|
<i class="fa fa-angle-down collapse-indicator"></i>
|
||||||
<i class="fa fa-angle-down collapse-indicator"></i>
|
|
||||||
</a>
|
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</summary>
|
||||||
<div id="{{ pid }}" class="panel-collapse collapse">
|
<div id="{{ pid }}">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{% with exclude|split as exclusion %}
|
{% with exclude|split as exclusion %}
|
||||||
{% with items|split as item_list %}
|
{% with items|split as item_list %}
|
||||||
@@ -51,4 +49,4 @@
|
|||||||
{% endwith %}
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</details>
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "General payment settings" %}</legend>
|
<legend>{% trans "General payment settings" %}</legend>
|
||||||
|
{% bootstrap_form_errors form layout="control" %}
|
||||||
{% bootstrap_field form.payment_term_days layout="control" %}
|
{% bootstrap_field form.payment_term_days layout="control" %}
|
||||||
{% bootstrap_field form.payment_term_last layout="control" %}
|
{% bootstrap_field form.payment_term_last layout="control" %}
|
||||||
{% bootstrap_field form.payment_term_weekdays layout="control" %}
|
{% bootstrap_field form.payment_term_weekdays layout="control" %}
|
||||||
|
|||||||
@@ -21,16 +21,15 @@
|
|||||||
{% bootstrap_field form.name layout="control" %}
|
{% bootstrap_field form.name layout="control" %}
|
||||||
{% bootstrap_field form.rate addon_after="%" layout="control" %}
|
{% bootstrap_field form.rate addon_after="%" layout="control" %}
|
||||||
|
|
||||||
<div class="panel panel-default">
|
<details class="panel panel-default"
|
||||||
<div class="panel-heading">
|
{% if rule.eu_reverse_charge or rule.has_custom_rules or form.errors %}open{% endif %}>
|
||||||
|
<summary class="panel-heading">
|
||||||
<h4 class="panel-title">
|
<h4 class="panel-title">
|
||||||
<a data-toggle="collapse" href="#advanced">
|
<strong>{% trans "Advanced settings" %}</strong>
|
||||||
<strong>{% trans "Advanced settings" %}</strong>
|
<i class="fa fa-angle-down collapse-indicator"></i>
|
||||||
<i class="fa fa-angle-down collapse-indicator"></i>
|
|
||||||
</a>
|
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</summary>
|
||||||
<div id="advanced" class="panel-collapse collapsed {% if rule.eu_reverse_charge or rule.has_custom_rules or form.errors %}in{% endif %}">
|
<div id="advanced">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<legend>{% trans "Advanced settings" %}</legend>
|
<legend>{% trans "Advanced settings" %}</legend>
|
||||||
<div class="alert alert-legal">
|
<div class="alert alert-legal">
|
||||||
@@ -52,6 +51,7 @@
|
|||||||
checked in order and once the first rule matches the order, it will be used and all further rules will
|
checked in order and once the first rule matches the order, it will be used and all further rules will
|
||||||
be ignored. If no rule matches, tax will be charged.
|
be ignored. If no rule matches, tax will be charged.
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
|
{% trans "All of these rules will only apply if an invoice address is set." %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
|
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
|
||||||
@@ -111,7 +111,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</details>
|
||||||
|
|
||||||
|
|
||||||
<div class="form-group submit-group">
|
<div class="form-group submit-group">
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user