forked from CGM_Public/pretix_original
Compare commits
341 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31d1fc31cd | ||
|
|
597211d83a | ||
|
|
17679d4304 | ||
|
|
0fb70c78a9 | ||
|
|
e254e90e49 | ||
|
|
9c6e5f025d | ||
|
|
3c86532218 | ||
|
|
3834ae566f | ||
|
|
6766b2b19e | ||
|
|
b6d2f67c7c | ||
|
|
e70f593a94 | ||
|
|
ed5726fc0c | ||
|
|
5400d26c60 | ||
|
|
0bb6104532 | ||
|
|
16aa403735 | ||
|
|
1c279a92a7 | ||
|
|
35985dcb11 | ||
|
|
b0dcbe31fa | ||
|
|
b3c3ee3b22 | ||
|
|
aab340fd87 | ||
|
|
1871324ef4 | ||
|
|
d799d560b7 | ||
|
|
01e2851a76 | ||
|
|
ef2a4244ed | ||
|
|
55539dc8e5 | ||
|
|
ef303bfcc4 | ||
|
|
fff9ac04a9 | ||
|
|
76d27fbfaa | ||
|
|
2b1123b487 | ||
|
|
3607d8706d | ||
|
|
31fdf8721b | ||
|
|
128a1f349a | ||
|
|
7d432f0639 | ||
|
|
1ffc799c4d | ||
|
|
25dd8f2e2f | ||
|
|
b121596e4b | ||
|
|
cf835df62e | ||
|
|
7a3b7d4f02 | ||
|
|
b151d8f455 | ||
|
|
06de74d877 | ||
|
|
2ae9e3e0d9 | ||
|
|
0c0fe58bbf | ||
|
|
7b1e1a48ef | ||
|
|
c7dd50de0d | ||
|
|
a1caa65776 | ||
|
|
260973345d | ||
|
|
2c9b2620ea | ||
|
|
909c80e710 | ||
|
|
5a218ae6a9 | ||
|
|
b498d45621 | ||
|
|
b02196434b | ||
|
|
c0edce7760 | ||
|
|
cc46d55f5e | ||
|
|
ea8abb8dab | ||
|
|
f765d094b4 | ||
|
|
86f222870d | ||
|
|
19b5270d76 | ||
|
|
db76b9b0ef | ||
|
|
d23e53873f | ||
|
|
c116a4b998 | ||
|
|
2471d4bca5 | ||
|
|
8e04dbdcca | ||
|
|
0928358396 | ||
|
|
23f783c15c | ||
|
|
edae96c84f | ||
|
|
242ebdfae9 | ||
|
|
0ee502abec | ||
|
|
29cb1e93d8 | ||
|
|
c89242855c | ||
|
|
61a1368ed2 | ||
|
|
ac3e00fa03 | ||
|
|
d9d0f7b6f3 | ||
|
|
ad5e2df3be | ||
|
|
ec34561815 | ||
|
|
e1540b1648 | ||
|
|
a6b265455d | ||
|
|
8a6334bd86 | ||
|
|
173a23722a | ||
|
|
ab8eb2a34d | ||
|
|
30dcda616b | ||
|
|
3eafec9d6e | ||
|
|
a5910016fd | ||
|
|
0a49b93b26 | ||
|
|
7449bea836 | ||
|
|
0fc4478332 | ||
|
|
0df4a6e7ed | ||
|
|
a37cd380c8 | ||
|
|
11b2bd8887 | ||
|
|
8986db0975 | ||
|
|
2921611cb1 | ||
|
|
785fb29513 | ||
|
|
81c3d7fa17 | ||
|
|
8ff963698d | ||
|
|
6da63e0169 | ||
|
|
f84903ae27 | ||
|
|
a0a7859b33 | ||
|
|
af23d6e4bf | ||
|
|
7e9c9beace | ||
|
|
ac2fc2de5c | ||
|
|
45e548873e | ||
|
|
f484eb65df | ||
|
|
027a785ab5 | ||
|
|
25b80cbb57 | ||
|
|
589fa0f9de | ||
|
|
6d2989d15a | ||
|
|
5bb27b29ae | ||
|
|
d17f8a71e6 | ||
|
|
b664cc712a | ||
|
|
d61e8a9204 | ||
|
|
f00012a63e | ||
|
|
bd238f76ce | ||
|
|
703ae97820 | ||
|
|
1a60c5ea64 | ||
|
|
1d3ac5f02f | ||
|
|
8d23d75dfd | ||
|
|
9a32668ee1 | ||
|
|
ca0407a133 | ||
|
|
1de77b0784 | ||
|
|
d0907d3dcf | ||
|
|
81cc4bd768 | ||
|
|
262639e063 | ||
|
|
dedd93fb89 | ||
|
|
45f94aee03 | ||
|
|
d36e7d033f | ||
|
|
b94bd277bf | ||
|
|
e5095185d9 | ||
|
|
d76ce47597 | ||
|
|
58717850c2 | ||
|
|
29d52d4fe5 | ||
|
|
34c9c40ddc | ||
|
|
39d05a6c40 | ||
|
|
b664222c62 | ||
|
|
1ee48a10b5 | ||
|
|
2431a8b767 | ||
|
|
af84354e51 | ||
|
|
b04de880fc | ||
|
|
11f3057f76 | ||
|
|
ba164c16f6 | ||
|
|
7ef766ddfa | ||
|
|
bcafcc7dd8 | ||
|
|
e5b7102abc | ||
|
|
3601dd6bee | ||
|
|
a1d5854fbf | ||
|
|
09544a688d | ||
|
|
58a5892cc0 | ||
|
|
c9af76b46e | ||
|
|
91753935cf | ||
|
|
23a52eb12a | ||
|
|
79ecb231f2 | ||
|
|
08de7f59a3 | ||
|
|
0de3c33bab | ||
|
|
a4ae8b0e66 | ||
|
|
be1bf81298 | ||
|
|
b7528ae1cf | ||
|
|
4f6712ccbe | ||
|
|
939335f94b | ||
|
|
c849276a35 | ||
|
|
8e9f0f07a1 | ||
|
|
389884d191 | ||
|
|
d8c2c82da7 | ||
|
|
c3ed3d4899 | ||
|
|
e9235cd433 | ||
|
|
975b6d800a | ||
|
|
ee260c8231 | ||
|
|
f7fddc05dd | ||
|
|
eaa61c7795 | ||
|
|
d4994258e6 | ||
|
|
9b50ec2d74 | ||
|
|
447b6b7fee | ||
|
|
40f763c999 | ||
|
|
6a3d05be9e | ||
|
|
766447f021 | ||
|
|
5fbeb90f00 | ||
|
|
c01cc85eda | ||
|
|
4a054da6ee | ||
|
|
583a2b6572 | ||
|
|
fbe025afb2 | ||
|
|
66e6191122 | ||
|
|
0f26f0787c | ||
|
|
62a86c9b4a | ||
|
|
07318be4c9 | ||
|
|
3c8ef2c620 | ||
|
|
a858f47220 | ||
|
|
381fa5e1cd | ||
|
|
1539eea664 | ||
|
|
3d41d1331a | ||
|
|
00848b3339 | ||
|
|
d174b11c6a | ||
|
|
a501ce496a | ||
|
|
de277cc959 | ||
|
|
12b3ae81d6 | ||
|
|
fcdb40dda0 | ||
|
|
f65cf8e86a | ||
|
|
12540238b7 | ||
|
|
398a30e33a | ||
|
|
3410640618 | ||
|
|
7b45cfccc2 | ||
|
|
33f503aea1 | ||
|
|
3fd650081b | ||
|
|
b622854be6 | ||
|
|
6d00daa9ee | ||
|
|
f27148998a | ||
|
|
4a0369cc37 | ||
|
|
76aaf61e19 | ||
|
|
dd1e5fa929 | ||
|
|
4a2516e303 | ||
|
|
cf06712eca | ||
|
|
6185d675f0 | ||
|
|
a53cd3abce | ||
|
|
ebe86a17fb | ||
|
|
d189b16ee7 | ||
|
|
d70ce4491a | ||
|
|
607ff48d70 | ||
|
|
4bab44ca85 | ||
|
|
a5cdb485d0 | ||
|
|
282ef792c4 | ||
|
|
6cd888a1dc | ||
|
|
2e5b80c83c | ||
|
|
4511110069 | ||
|
|
1af1d8c658 | ||
|
|
9f6a3f9a6a | ||
|
|
1c03d5d305 | ||
|
|
69a1fccd20 | ||
|
|
2a54aa2d83 | ||
|
|
2269c8dee0 | ||
|
|
46295ea887 | ||
|
|
e41863229b | ||
|
|
ca5a6ddba1 | ||
|
|
4d4dafb5dd | ||
|
|
9c2af952b7 | ||
|
|
dc6e425c2a | ||
|
|
5f65b9528f | ||
|
|
8957c2f106 | ||
|
|
2bbbc88a9c | ||
|
|
162b7c1b52 | ||
|
|
755f3d53b6 | ||
|
|
f6db62d6ce | ||
|
|
aa1ffc402c | ||
|
|
2c9b96f0c5 | ||
|
|
16599e242d | ||
|
|
19c13d7f38 | ||
|
|
65db8cd583 | ||
|
|
d0794d7b94 | ||
|
|
a770f5a8e7 | ||
|
|
80a3063799 | ||
|
|
34ec11ecfa | ||
|
|
a1da2eafdc | ||
|
|
6bc2175ea9 | ||
|
|
21dcb4f43d | ||
|
|
e9722bcdbd | ||
|
|
e7eb8e3111 | ||
|
|
a895d83764 | ||
|
|
b6697b838b | ||
|
|
0d8c4271a9 | ||
|
|
d226bbda5c | ||
|
|
38d0198dea | ||
|
|
0a920ac21c | ||
|
|
7acee9458d | ||
|
|
82e40ce664 | ||
|
|
4632269ac3 | ||
|
|
6d7e1ef53d | ||
|
|
3ea4cdc3b3 | ||
|
|
e4619eeca3 | ||
|
|
bb5c7c5ad7 | ||
|
|
9984fe97ba | ||
|
|
242dd24caa | ||
|
|
2482d9390a | ||
|
|
3b4923ccae | ||
|
|
8a2e4385ff | ||
|
|
e83b8ac218 | ||
|
|
b387fba5f4 | ||
|
|
da68cb618e | ||
|
|
eb11dac21e | ||
|
|
6e531ee067 | ||
|
|
c8e6daa7a1 | ||
|
|
b3e3d427cb | ||
|
|
6e88054af7 | ||
|
|
22dfa0e61d | ||
|
|
833cd32578 | ||
|
|
fd1c964c92 | ||
|
|
87b10ef055 | ||
|
|
734f65b10b | ||
|
|
0f826a6f76 | ||
|
|
35e521cc55 | ||
|
|
63c845574f | ||
|
|
5a675cc75d | ||
|
|
994dc9bf76 | ||
|
|
cc4a07e3b0 | ||
|
|
2ca88d5328 | ||
|
|
0bca9b9bf1 | ||
|
|
742d2f11be | ||
|
|
5ea5b82994 | ||
|
|
81245cf125 | ||
|
|
c6bcd05404 | ||
|
|
1999a25095 | ||
|
|
62f7c5ba0f | ||
|
|
d11b0e92f1 | ||
|
|
662bdea45b | ||
|
|
d37cc4f641 | ||
|
|
6b2bc71be9 | ||
|
|
f267940562 | ||
|
|
7140406f35 | ||
|
|
e275e2e240 | ||
|
|
75c0920f5e | ||
|
|
b6efe9ae1e | ||
|
|
a28378bac9 | ||
|
|
a940fa9eb7 | ||
|
|
332fba6168 | ||
|
|
41655532e9 | ||
|
|
6cc9801fe1 | ||
|
|
29ff5b9416 | ||
|
|
889dd651ef | ||
|
|
8c7d7a3055 | ||
|
|
ff67931c04 | ||
|
|
faa6f0e0a3 | ||
|
|
68ec37605f | ||
|
|
2ef8b89da0 | ||
|
|
dfc746ea7a | ||
|
|
661546f130 | ||
|
|
5e61342ff5 | ||
|
|
4eadfdeec2 | ||
|
|
8284a9de44 | ||
|
|
ae2e70245f | ||
|
|
a92e283a66 | ||
|
|
9e2c0d8152 | ||
|
|
57453a5b00 | ||
|
|
1ccf677ea2 | ||
|
|
0a9daf0d3a | ||
|
|
934217ee4f | ||
|
|
deff282a63 | ||
|
|
bcd687764c | ||
|
|
8d7224fecc | ||
|
|
3fff3378c0 | ||
|
|
91ae89d463 | ||
|
|
5c0d112def | ||
|
|
f7ae90811e | ||
|
|
6ec8c33ecc | ||
|
|
f991d5434f | ||
|
|
7cf1688de5 | ||
|
|
298b3c3660 | ||
|
|
5ea1c96e19 |
18
.travis.yml
18
.travis.yml
@@ -13,24 +13,24 @@ services:
|
|||||||
- postgresql
|
- postgresql
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- python: 3.7
|
- python: 3.8
|
||||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
|
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
|
||||||
- python: 3.7
|
- python: 3.8
|
||||||
env: JOB=tests-cov PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
env: JOB=tests-cov PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||||
- python: 3.7
|
- python: 3.8
|
||||||
env: JOB=style
|
env: JOB=style
|
||||||
- python: 3.7
|
- python: 3.8
|
||||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
|
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
|
||||||
- python: 3.7
|
- python: 3.8
|
||||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
|
||||||
- python: 3.5
|
|
||||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||||
- python: 3.7
|
- python: 3.7
|
||||||
|
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||||
|
- python: 3.8
|
||||||
env: JOB=doc-spelling
|
env: JOB=doc-spelling
|
||||||
- python: 3.7
|
- python: 3.8
|
||||||
env: JOB=translation-spelling
|
env: JOB=translation-spelling
|
||||||
addons:
|
addons:
|
||||||
postgresql: "9.4"
|
postgresql: "10"
|
||||||
mariadb: '10.3'
|
mariadb: '10.3'
|
||||||
apt:
|
apt:
|
||||||
packages:
|
packages:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ pid /var/run/nginx.pid;
|
|||||||
daemon off;
|
daemon off;
|
||||||
|
|
||||||
events {
|
events {
|
||||||
worker_connections 768;
|
worker_connections 4096;
|
||||||
}
|
}
|
||||||
|
|
||||||
http {
|
http {
|
||||||
@@ -39,7 +39,7 @@ http {
|
|||||||
include /etc/nginx/conf.d/*.conf;
|
include /etc/nginx/conf.d/*.conf;
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80 default_server;
|
listen 80 backlog=4096 default_server;
|
||||||
listen [::]:80 ipv6only=on default_server;
|
listen [::]:80 ipv6only=on default_server;
|
||||||
server_name _;
|
server_name _;
|
||||||
index index.php index.html;
|
index index.php index.html;
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ named ``/etc/systemd/system/pretix.service`` with the following content::
|
|||||||
-v /var/pretix-data:/data \
|
-v /var/pretix-data:/data \
|
||||||
-v /etc/pretix:/etc/pretix \
|
-v /etc/pretix:/etc/pretix \
|
||||||
-v /var/run/redis:/var/run/redis \
|
-v /var/run/redis:/var/run/redis \
|
||||||
|
--sysctl net.core.somaxconn=4096 \
|
||||||
pretix/standalone:stable all
|
pretix/standalone:stable all
|
||||||
ExecStop=/usr/bin/docker stop %n
|
ExecStop=/usr/bin/docker stop %n
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ solution with many things readily set-up, look at :ref:`dockersmallscale`.
|
|||||||
get it right. If you're not feeling comfortable managing a Linux server, check out our hosting and service
|
get it right. If you're not feeling comfortable managing a Linux server, check out our hosting and service
|
||||||
offers at `pretix.eu`_.
|
offers at `pretix.eu`_.
|
||||||
|
|
||||||
We tested this guide on the Linux distribution **Debian 8.0** but it should work very similar on other
|
We tested this guide on the Linux distribution **Debian 10.0** but it should work very similar on other
|
||||||
modern distributions, especially on all systemd-based ones.
|
modern distributions, especially on all systemd-based ones.
|
||||||
|
|
||||||
Requirements
|
Requirements
|
||||||
@@ -133,7 +133,7 @@ command if you're running MySQL::
|
|||||||
|
|
||||||
(venv)$ pip3 install "pretix[postgres]" gunicorn
|
(venv)$ pip3 install "pretix[postgres]" gunicorn
|
||||||
|
|
||||||
Note that you need Python 3.5 or newer. You can find out your Python version using ``python -V``.
|
Note that you need Python 3.6 or newer. You can find out your Python version using ``python -V``.
|
||||||
|
|
||||||
We also need to create a data directory::
|
We also need to create a data directory::
|
||||||
|
|
||||||
|
|||||||
@@ -170,6 +170,19 @@ Date String in ISO 8601 format ``2017-12-27``
|
|||||||
Multi-lingual string Object of strings ``{"en": "red", "de": "rot", "de_Informal": "rot"}``
|
Multi-lingual string Object of strings ``{"en": "red", "de": "rot", "de_Informal": "rot"}``
|
||||||
Money String with decimal number ``"23.42"``
|
Money String with decimal number ``"23.42"``
|
||||||
Currency String with ISO 4217 code ``"EUR"``, ``"USD"``
|
Currency String with ISO 4217 code ``"EUR"``, ``"USD"``
|
||||||
|
Relative datetime *either* String in ISO 8601 ``"2017-12-27T10:00:00.596934Z"``,
|
||||||
|
format *or* specification of ``"RELDATE/3/12:00:00/presale_start/"``
|
||||||
|
a relative datetime,
|
||||||
|
constructed from a number of
|
||||||
|
days before the base point,
|
||||||
|
a time of day, and the base
|
||||||
|
point.
|
||||||
|
Relative date *either* String in ISO 8601 ``"2017-12-27"``,
|
||||||
|
format *or* specification of ``"RELDATE/3/-/presale_start/"``
|
||||||
|
a relative date,
|
||||||
|
constructed from a number of
|
||||||
|
days before the base point
|
||||||
|
and the base point.
|
||||||
===================== ============================ ===================================
|
===================== ============================ ===================================
|
||||||
|
|
||||||
Query parameters
|
Query parameters
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ access to the API. The ``token`` endpoint expects you to authenticate using `HTT
|
|||||||
ID as a username and your client secret as a password. You are also required to again supply the same ``redirect_uri``
|
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.
|
parameter that you used for the authorization.
|
||||||
|
|
||||||
.. http:get:: /api/v1/oauth/token
|
.. http:post:: /api/v1/oauth/token
|
||||||
|
|
||||||
Request a new access token
|
Request a new access token
|
||||||
|
|
||||||
|
|||||||
148
doc/api/resources/billing_var.rst
Normal file
148
doc/api/resources/billing_var.rst
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
pretix Hosted reseller API
|
||||||
|
==========================
|
||||||
|
|
||||||
|
This API is only accessible to our `value-added reseller partners`_ on pretix Hosted.
|
||||||
|
|
||||||
|
.. note:: This API is only accessible with user-level permissions, not with API tokens. Therefore, you will need to
|
||||||
|
create an :ref:`OAuth application <rest-oauth>` and obtain an OAuth access token for a user account that has
|
||||||
|
permission to your reseller account.
|
||||||
|
|
||||||
|
Reseller account resource
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
The resource contains the following public fields:
|
||||||
|
|
||||||
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
Field Type Description
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
id integer Your reseller ID
|
||||||
|
name string Internal name of your reseller account
|
||||||
|
public_name string Public name of your reseller account
|
||||||
|
public_url string Public URL of your company
|
||||||
|
support_email string Your support email address
|
||||||
|
support_phone string Your support phone number
|
||||||
|
communication_language string Language code we use to communicate with you
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|
||||||
|
Endpoints
|
||||||
|
---------
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/var/
|
||||||
|
|
||||||
|
Returns a list of all reseller accounts you have access to.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/var/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "ticketshop.live Ltd & Co. KG",
|
||||||
|
"public_name": "ticketshop.live",
|
||||||
|
"public_url": "https://ticketshop.live",
|
||||||
|
"support_email": "support@ticketshop.live",
|
||||||
|
"support_phone": "+4962213217750",
|
||||||
|
"communication_language": "de"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
: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
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/var/(id)/
|
||||||
|
|
||||||
|
Returns information on one reseller account, identified by its ID.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/var/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,
|
||||||
|
"name": "ticketshop.live Ltd & Co. KG",
|
||||||
|
"public_name": "ticketshop.live",
|
||||||
|
"public_url": "https://ticketshop.live",
|
||||||
|
"support_email": "support@ticketshop.live",
|
||||||
|
"support_phone": "+4962213217750",
|
||||||
|
"communication_language": "de"
|
||||||
|
}
|
||||||
|
|
||||||
|
:param id: The ``id`` field of the reseller account to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 404: The requested account does not exist **or** you have no permission to view this resource.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/var/(id)/create_organizer/
|
||||||
|
|
||||||
|
Creates a new organizer account that will be associated with a given reseller account.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/var/1/create_organizer/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
Content-Length: 123
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "My new client",
|
||||||
|
"slug": "New client"
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "My new client",
|
||||||
|
"slug": "New client"
|
||||||
|
}
|
||||||
|
|
||||||
|
:param id: The ``id`` field of the reseller account to fetch
|
||||||
|
:statuscode 201: no error
|
||||||
|
:statuscode 400: Invalid request body, usually the slug is invalid or already taken.
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 404: The requested account does not exist **or** you have no permission to view this resource.
|
||||||
|
|
||||||
|
.. _value-added reseller partners: https://pretix.eu/about/en/var
|
||||||
@@ -43,6 +43,7 @@ seating_plan integer If reserved sea
|
|||||||
seat_category_mapping object An object mapping categories of the seating plan
|
seat_category_mapping object An object mapping categories of the seating plan
|
||||||
(strings) to items in the event (integers or ``null``).
|
(strings) to items in the event (integers or ``null``).
|
||||||
timezone string Event timezone name
|
timezone string Event timezone name
|
||||||
|
item_meta_properties object Item-specific meta data parameters and default values.
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|
||||||
@@ -79,6 +80,10 @@ timezone string Event timezone
|
|||||||
|
|
||||||
The attribute ``timezone`` has been added.
|
The attribute ``timezone`` has been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.7
|
||||||
|
|
||||||
|
The attribute ``item_meta_properties`` has been added.
|
||||||
|
|
||||||
Endpoints
|
Endpoints
|
||||||
---------
|
---------
|
||||||
|
|
||||||
@@ -133,6 +138,7 @@ Endpoints
|
|||||||
"seating_plan": null,
|
"seating_plan": null,
|
||||||
"seat_category_mapping": {},
|
"seat_category_mapping": {},
|
||||||
"timezone": "Europe/Berlin",
|
"timezone": "Europe/Berlin",
|
||||||
|
"item_meta_properties": {},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"pretix.plugins.banktransfer"
|
"pretix.plugins.banktransfer"
|
||||||
"pretix.plugins.stripe"
|
"pretix.plugins.stripe"
|
||||||
@@ -204,6 +210,7 @@ Endpoints
|
|||||||
"seat_category_mapping": {},
|
"seat_category_mapping": {},
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
"timezone": "Europe/Berlin",
|
"timezone": "Europe/Berlin",
|
||||||
|
"item_meta_properties": {},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"pretix.plugins.banktransfer"
|
"pretix.plugins.banktransfer"
|
||||||
"pretix.plugins.stripe"
|
"pretix.plugins.stripe"
|
||||||
@@ -256,6 +263,7 @@ Endpoints
|
|||||||
"has_subevents": false,
|
"has_subevents": false,
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
"timezone": "Europe/Berlin",
|
"timezone": "Europe/Berlin",
|
||||||
|
"item_meta_properties": {},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"pretix.plugins.stripe",
|
"pretix.plugins.stripe",
|
||||||
"pretix.plugins.paypal"
|
"pretix.plugins.paypal"
|
||||||
@@ -290,6 +298,7 @@ Endpoints
|
|||||||
"has_subevents": false,
|
"has_subevents": false,
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
"timezone": "Europe/Berlin",
|
"timezone": "Europe/Berlin",
|
||||||
|
"item_meta_properties": {},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"pretix.plugins.stripe",
|
"pretix.plugins.stripe",
|
||||||
"pretix.plugins.paypal"
|
"pretix.plugins.paypal"
|
||||||
@@ -344,6 +353,7 @@ Endpoints
|
|||||||
"has_subevents": false,
|
"has_subevents": false,
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
"timezone": "Europe/Berlin",
|
"timezone": "Europe/Berlin",
|
||||||
|
"item_meta_properties": {},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"pretix.plugins.stripe",
|
"pretix.plugins.stripe",
|
||||||
"pretix.plugins.paypal"
|
"pretix.plugins.paypal"
|
||||||
@@ -378,6 +388,7 @@ Endpoints
|
|||||||
"seat_category_mapping": {},
|
"seat_category_mapping": {},
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
"timezone": "Europe/Berlin",
|
"timezone": "Europe/Berlin",
|
||||||
|
"item_meta_properties": {},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"pretix.plugins.stripe",
|
"pretix.plugins.stripe",
|
||||||
"pretix.plugins.paypal"
|
"pretix.plugins.paypal"
|
||||||
@@ -444,6 +455,7 @@ Endpoints
|
|||||||
"seat_category_mapping": {},
|
"seat_category_mapping": {},
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
"timezone": "Europe/Berlin",
|
"timezone": "Europe/Berlin",
|
||||||
|
"item_meta_properties": {},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"pretix.plugins.banktransfer",
|
"pretix.plugins.banktransfer",
|
||||||
"pretix.plugins.stripe",
|
"pretix.plugins.stripe",
|
||||||
@@ -486,3 +498,123 @@ Endpoints
|
|||||||
:statuscode 204: no error
|
:statuscode 204: 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 delete this resource.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
|
||||||
|
|
||||||
|
Event settings
|
||||||
|
--------------
|
||||||
|
|
||||||
|
pretix events have lots and lots of parameters of different types that are stored in a key-value store on our system.
|
||||||
|
Since many of these settings depend on each other in complex ways, we can not give direct access to all of these
|
||||||
|
settings through the API. However, we do expose many of the simple and useful flags through the API.
|
||||||
|
|
||||||
|
Please note that the available settings flags change between pretix versions and also between events, depending on the
|
||||||
|
installed plugins, and we do not give a guarantee on backwards-compatibility like with other parts of the API.
|
||||||
|
Therefore, we're also not including a list of the options here, but instead recommend to look at the endpoint output
|
||||||
|
to see available options. The ``explain=true`` flag enables a verbose mode that provides you with human-readable
|
||||||
|
information about the properties.
|
||||||
|
|
||||||
|
.. note:: Please note that this is not a complete representation of all event settings. You will find more settings
|
||||||
|
in the web interface.
|
||||||
|
|
||||||
|
.. warning:: This API is intended for advanced users. Even though we take care to validate your input, you will be
|
||||||
|
able to break your event using this API by creating situations of conflicting settings. Please take care.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.6
|
||||||
|
|
||||||
|
Initial support for settings has been added to the API.
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/settings/
|
||||||
|
|
||||||
|
Get current values of event settings.
|
||||||
|
|
||||||
|
Permission required: "Can change event settings"
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/settings/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example standard response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"imprint_url": "https://pretix.eu",
|
||||||
|
…
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example verbose response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"imprint_url":
|
||||||
|
{
|
||||||
|
"value": "https://pretix.eu",
|
||||||
|
"label": "Imprint URL",
|
||||||
|
"help_text": "This should point e.g. to a part of your website that has your contact details and legal information."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
…
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer of the event to access
|
||||||
|
:param event: The ``slug`` field of the event to access
|
||||||
|
:query explain: Set to ``true`` to enable verbose response mode
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||||
|
|
||||||
|
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/settings/
|
||||||
|
|
||||||
|
Updates event settings. Note that ``PUT`` is not allowed here, only ``PATCH``.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
Settings can be stored at different levels in pretix. If a value is not set on event level, a default setting
|
||||||
|
from a higher level (organizer, global) will be returned. If you explicitly set a setting on event level, it
|
||||||
|
will no longer be inherited from the higher levels. Therefore, we recommend you to send only settings that you
|
||||||
|
explicitly want to set on event level. To unset a settings, pass ``null``.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
PATCH /api/v1/organizers/bigevents/events/sampleconf/settings/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"imprint_url": "https://example.org/imprint/"
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"imprint_url": "https://example.org/imprint/",
|
||||||
|
…
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer of the event to update
|
||||||
|
:param event: The ``slug`` field of the event to update
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 400: The event could not be updated due to invalid submitted data.
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ Endpoints
|
|||||||
}
|
}
|
||||||
|
|
||||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:query string secret: Only show gift cards with the given secret.
|
||||||
|
:query boolean testmode: Filter for gift cards that are (not) in test mode.
|
||||||
|
:query boolean include_accepted: Also show gift cards issued by other organizers that are accepted by this organizer.
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
@@ -94,6 +97,7 @@ Endpoints
|
|||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
:param id: The ``id`` field of the gift card to fetch
|
:param id: The ``id`` field of the gift card to fetch
|
||||||
|
:query boolean include_accepted: Also show gift cards issued by other organizers that are accepted by this organizer.
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||||
@@ -227,6 +231,7 @@ Endpoints
|
|||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to modify
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
:param id: The ``id`` field of the gift card to modify
|
:param id: The ``id`` field of the gift card to modify
|
||||||
|
:query boolean include_accepted: Also show gift cards issued by other organizers that are accepted by this organizer.
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 400: The gift card could not be modified due to invalid submitted data
|
:statuscode 400: The gift card could not be modified due to invalid submitted data
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ Resources and endpoints
|
|||||||
waitinglist
|
waitinglist
|
||||||
giftcards
|
giftcards
|
||||||
carts
|
carts
|
||||||
|
teams
|
||||||
webhooks
|
webhooks
|
||||||
seatingplans
|
seatingplans
|
||||||
billing_invoices
|
billing_invoices
|
||||||
|
billing_var
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ bundles list of objects Definition of b
|
|||||||
└ designated_price money (string) Designated price of the bundled product. This will be
|
└ designated_price money (string) Designated price of the bundled product. This will be
|
||||||
used to split the price of the base item e.g. for mixed
|
used to split the price of the base item e.g. for mixed
|
||||||
taxation. This is not added to the price.
|
taxation. This is not added to the price.
|
||||||
|
meta_data object Values set for event-specific meta data parameters.
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
.. versionchanged:: 2.7
|
.. versionchanged:: 2.7
|
||||||
@@ -154,6 +155,10 @@ bundles list of objects Definition of b
|
|||||||
|
|
||||||
The ``show_quota_left``, ``allow_waitinglist``, and ``hidden_if_available`` attributes have been added.
|
The ``show_quota_left``, ``allow_waitinglist``, and ``hidden_if_available`` attributes have been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.7
|
||||||
|
|
||||||
|
The attribute ``meta_data`` has been added.
|
||||||
|
|
||||||
Notes
|
Notes
|
||||||
-----
|
-----
|
||||||
|
|
||||||
@@ -208,6 +213,7 @@ Endpoints
|
|||||||
"tax_rule": 1,
|
"tax_rule": 1,
|
||||||
"admission": false,
|
"admission": false,
|
||||||
"issue_giftcard": false,
|
"issue_giftcard": false,
|
||||||
|
"meta_data": {},
|
||||||
"position": 0,
|
"position": 0,
|
||||||
"picture": null,
|
"picture": null,
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
@@ -303,6 +309,7 @@ Endpoints
|
|||||||
"tax_rule": 1,
|
"tax_rule": 1,
|
||||||
"admission": false,
|
"admission": false,
|
||||||
"issue_giftcard": false,
|
"issue_giftcard": false,
|
||||||
|
"meta_data": {},
|
||||||
"position": 0,
|
"position": 0,
|
||||||
"picture": null,
|
"picture": null,
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
@@ -379,6 +386,7 @@ Endpoints
|
|||||||
"tax_rule": 1,
|
"tax_rule": 1,
|
||||||
"admission": false,
|
"admission": false,
|
||||||
"issue_giftcard": false,
|
"issue_giftcard": false,
|
||||||
|
"meta_data": {},
|
||||||
"position": 0,
|
"position": 0,
|
||||||
"picture": null,
|
"picture": null,
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
@@ -442,6 +450,7 @@ Endpoints
|
|||||||
"tax_rule": 1,
|
"tax_rule": 1,
|
||||||
"admission": false,
|
"admission": false,
|
||||||
"issue_giftcard": false,
|
"issue_giftcard": false,
|
||||||
|
"meta_data": {},
|
||||||
"position": 0,
|
"position": 0,
|
||||||
"picture": null,
|
"picture": null,
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
@@ -537,6 +546,7 @@ Endpoints
|
|||||||
"tax_rule": 1,
|
"tax_rule": 1,
|
||||||
"admission": false,
|
"admission": false,
|
||||||
"issue_giftcard": false,
|
"issue_giftcard": false,
|
||||||
|
"meta_data": {},
|
||||||
"position": 0,
|
"position": 0,
|
||||||
"picture": null,
|
"picture": null,
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
|||||||
@@ -151,6 +151,10 @@ last_modified datetime Last modificati
|
|||||||
|
|
||||||
The ``order.fees.canceled`` attribute has been added.
|
The ``order.fees.canceled`` attribute has been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.8
|
||||||
|
|
||||||
|
The ``reactivate`` operation has been added.
|
||||||
|
|
||||||
|
|
||||||
.. _order-position-resource:
|
.. _order-position-resource:
|
||||||
|
|
||||||
@@ -173,6 +177,13 @@ price money (string) Price of this p
|
|||||||
attendee_name string Specified attendee name for this position (or ``null``)
|
attendee_name string Specified attendee name for this position (or ``null``)
|
||||||
attendee_name_parts object of strings Decomposition of attendee name (i.e. given name, family name)
|
attendee_name_parts object of strings Decomposition of attendee name (i.e. given name, family name)
|
||||||
attendee_email string Specified attendee email address for this position (or ``null``)
|
attendee_email string Specified attendee email address for this position (or ``null``)
|
||||||
|
company string Attendee company name (or ``null``)
|
||||||
|
street string Attendee street (or ``null``)
|
||||||
|
zipcode string Attendee ZIP code (or ``null``)
|
||||||
|
city string Attendee city (or ``null``)
|
||||||
|
country string Attendee country code (or ``null``)
|
||||||
|
state string Attendee state (ISO 3166-2 code). Only supported in
|
||||||
|
AU, BR, CA, CN, MY, MX, and US, otherwise ``null``.
|
||||||
voucher integer Internal ID of the voucher used for this position (or ``null``)
|
voucher integer Internal ID of the voucher used for this position (or ``null``)
|
||||||
tax_rate decimal (string) VAT rate applied for this position
|
tax_rate decimal (string) VAT rate applied for this position
|
||||||
tax_value money (string) VAT included in this position
|
tax_value money (string) VAT included in this position
|
||||||
@@ -236,6 +247,10 @@ pdf_data object Data object req
|
|||||||
|
|
||||||
The attribute ``canceled`` has been added.
|
The attribute ``canceled`` has been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.8
|
||||||
|
|
||||||
|
The attributes ``company``, ``street``, ``zipcode``, ``city``, ``country``, and ``state`` have been added.
|
||||||
|
|
||||||
.. _order-payment-resource:
|
.. _order-payment-resource:
|
||||||
|
|
||||||
Order payment resource
|
Order payment resource
|
||||||
@@ -380,6 +395,12 @@ List of all orders
|
|||||||
"full_name": "Peter",
|
"full_name": "Peter",
|
||||||
},
|
},
|
||||||
"attendee_email": null,
|
"attendee_email": null,
|
||||||
|
"company": "Sample company",
|
||||||
|
"street": "Test street 12",
|
||||||
|
"zipcode": "12345",
|
||||||
|
"city": "Testington",
|
||||||
|
"country": "DE",
|
||||||
|
"state": null,
|
||||||
"voucher": null,
|
"voucher": null,
|
||||||
"tax_rate": "0.00",
|
"tax_rate": "0.00",
|
||||||
"tax_value": "0.00",
|
"tax_value": "0.00",
|
||||||
@@ -536,6 +557,12 @@ Fetching individual orders
|
|||||||
"full_name": "Peter",
|
"full_name": "Peter",
|
||||||
},
|
},
|
||||||
"attendee_email": null,
|
"attendee_email": null,
|
||||||
|
"company": "Sample company",
|
||||||
|
"street": "Test street 12",
|
||||||
|
"zipcode": "12345",
|
||||||
|
"city": "Testington",
|
||||||
|
"country": "DE",
|
||||||
|
"state": null,
|
||||||
"voucher": null,
|
"voucher": null,
|
||||||
"tax_rate": "0.00",
|
"tax_rate": "0.00",
|
||||||
"tax_rule": null,
|
"tax_rule": null,
|
||||||
@@ -816,9 +843,9 @@ Creating orders
|
|||||||
* ``consume_carts`` (optional) – A list of cart IDs. All cart positions with these IDs will be deleted if the
|
* ``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 or seats that become free by this operation will be credited to your order
|
order creation is successful. Any quotas or seats that become free by this operation will be credited to your order
|
||||||
creation.
|
creation.
|
||||||
* ``email``
|
* ``email`` (optional)
|
||||||
* ``locale``
|
* ``locale``
|
||||||
* ``sales_channel``
|
* ``sales_channel`` (optional)
|
||||||
* ``payment_provider`` (optional) – The identifier of the payment provider set for this order. This needs to be an
|
* ``payment_provider`` (optional) – 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, and we strongly advise to use ``"manual"``
|
existing payment provider. You should use ``"free"`` for free orders, and we strongly advise to use ``"manual"``
|
||||||
for all orders you create as paid. This field is optional when the order status is ``"n"`` or the order total is
|
for all orders you create as paid. This field is optional when the order status is ``"n"`` or the order total is
|
||||||
@@ -851,15 +878,21 @@ Creating orders
|
|||||||
|
|
||||||
* ``positionid`` (optional, see below)
|
* ``positionid`` (optional, see below)
|
||||||
* ``item``
|
* ``item``
|
||||||
* ``variation``
|
* ``variation`` (optional)
|
||||||
* ``price`` (optional, if set to ``null`` or missing the price will be computed from the given product)
|
* ``price`` (optional, if set to ``null`` or missing the price will be computed from the given product)
|
||||||
* ``seat`` (The ``seat_guid`` attribute of a seat. Required when the specified ``item`` requires a seat, otherwise must be ``null``.)
|
* ``seat`` (The ``seat_guid`` attribute of a seat. Required when the specified ``item`` requires a seat, otherwise must be ``null``.)
|
||||||
* ``attendee_name`` **or** ``attendee_name_parts``
|
* ``attendee_name`` **or** ``attendee_name_parts`` (optional)
|
||||||
* ``voucher`` (optional, the ``code`` attribute of a valid voucher)
|
* ``voucher`` (optional, the ``code`` attribute of a valid voucher)
|
||||||
* ``attendee_email``
|
* ``attendee_email`` (optional)
|
||||||
|
* ``company`` (optional)
|
||||||
|
* ``street`` (optional)
|
||||||
|
* ``zipcode`` (optional)
|
||||||
|
* ``city`` (optional)
|
||||||
|
* ``country`` (optional)
|
||||||
|
* ``state`` (optional)
|
||||||
* ``secret`` (optional)
|
* ``secret`` (optional)
|
||||||
* ``addon_to`` (optional, see below)
|
* ``addon_to`` (optional, see below)
|
||||||
* ``subevent``
|
* ``subevent`` (optional)
|
||||||
* ``answers``
|
* ``answers``
|
||||||
|
|
||||||
* ``question``
|
* ``question``
|
||||||
@@ -891,6 +924,13 @@ Creating orders
|
|||||||
IDs in the ``addon_to`` field of another position. Note that all add_ons for a specific position need to come
|
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.
|
immediately after the position itself.
|
||||||
|
|
||||||
|
Starting with pretix 3.7, you can add ``"simulate": true`` to the body to do a "dry run" of your order. This will
|
||||||
|
validate your order and return you an order object with the resulting prices, but will not create an actual order.
|
||||||
|
You can use this for testing or to look up prices. In this case, some attributes are ignored, such as whether
|
||||||
|
to send an email or what payment provider will be used. Note that some returned fields will contain empty values
|
||||||
|
(e.g. all ``id`` fields of positions will be zero) and some will contain fake values (e.g. the order code will
|
||||||
|
always be ``PREVIEW``). pretix plugins will not be triggered, so some special behavior might be missing as well.
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
@@ -1050,6 +1090,42 @@ Order state operations
|
|||||||
: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)/reactivate/
|
||||||
|
|
||||||
|
Reactivates a canceled order. This will set the order to pending or paid state. Only possible if all products are
|
||||||
|
still available.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/reactivate/ 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": "n",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
: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 reactivated
|
||||||
|
: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_pending/
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/mark_pending/
|
||||||
|
|
||||||
Marks a paid order as unpaid.
|
Marks a paid order as unpaid.
|
||||||
@@ -1621,6 +1697,10 @@ Order payment endpoints
|
|||||||
|
|
||||||
These endpoints have been added.
|
These endpoints have been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.6
|
||||||
|
|
||||||
|
Payments can now be created through the API.
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/payments/
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/payments/
|
||||||
|
|
||||||
Returns a list of all payments for an order.
|
Returns a list of all payments for an order.
|
||||||
@@ -1829,6 +1909,61 @@ Order payment 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 or payment does not exist.
|
:statuscode 404: The requested order or payment does not exist.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/payments/
|
||||||
|
|
||||||
|
Creates a new payment.
|
||||||
|
|
||||||
|
Be careful with the ``info`` parameter: You can pass a nested JSON object that will be set as the internal ``info``
|
||||||
|
value of the payment object that will be created. 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*.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/payments/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"state": "confirmed",
|
||||||
|
"amount": "23.00",
|
||||||
|
"payment_date": "2017-12-04T12:13:12Z",
|
||||||
|
"info": {},
|
||||||
|
"provider": "banktransfer"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"local_id": 1,
|
||||||
|
"state": "confirmed",
|
||||||
|
"amount": "23.00",
|
||||||
|
"created": "2017-12-01T10:00:00Z",
|
||||||
|
"payment_date": "2017-12-04T12:13:12Z",
|
||||||
|
"payment_url": null,
|
||||||
|
"details": {},
|
||||||
|
"provider": "banktransfer"
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to access
|
||||||
|
:param event: The ``slug`` field of the event to access
|
||||||
|
:param order: The ``code`` field of the order to access
|
||||||
|
:statuscode 201: 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 order does not exist.
|
||||||
|
|
||||||
|
|
||||||
Order refund endpoints
|
Order refund endpoints
|
||||||
----------------------
|
----------------------
|
||||||
@@ -1947,7 +2082,8 @@ Order refund endpoints
|
|||||||
"payment": 1,
|
"payment": 1,
|
||||||
"execution_date": null,
|
"execution_date": null,
|
||||||
"provider": "manual",
|
"provider": "manual",
|
||||||
"mark_canceled": false
|
"mark_canceled": false,
|
||||||
|
"mark_pending": true
|
||||||
}
|
}
|
||||||
|
|
||||||
**Example response**:
|
**Example response**:
|
||||||
|
|||||||
671
doc/api/resources/teams.rst
Normal file
671
doc/api/resources/teams.rst
Normal file
@@ -0,0 +1,671 @@
|
|||||||
|
.. spelling:: fullname
|
||||||
|
|
||||||
|
.. _`rest-teams`:
|
||||||
|
|
||||||
|
Teams
|
||||||
|
=====
|
||||||
|
|
||||||
|
.. warning:: Unlike our user interface, the team API **does** allow you to lock yourself out by deleting or modifying
|
||||||
|
the team your user or API key belongs to. Be careful around here!
|
||||||
|
|
||||||
|
Team resource
|
||||||
|
-------------
|
||||||
|
|
||||||
|
The team resource contains the following public fields:
|
||||||
|
|
||||||
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
Field Type Description
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
id integer Internal ID of the team
|
||||||
|
name string Team name
|
||||||
|
all_events boolean Whether this team has access to all events
|
||||||
|
limit_events list List of event slugs this team has access to
|
||||||
|
can_create_events boolean
|
||||||
|
can_change_teams boolean
|
||||||
|
can_change_organizer_settings boolean
|
||||||
|
can_manage_gift_cards boolean
|
||||||
|
can_change_event_settings boolean
|
||||||
|
can_change_items boolean
|
||||||
|
can_view_orders boolean
|
||||||
|
can_change_orders boolean
|
||||||
|
can_view_vouchers boolean
|
||||||
|
can_change_vouchers boolean
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
Team member resource
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
The team member resource contains the following public fields:
|
||||||
|
|
||||||
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
Field Type Description
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
id integer Internal ID of the user
|
||||||
|
email string The user's email address
|
||||||
|
fullname string The user's full name (or ``null``)
|
||||||
|
require_2fa boolean Whether this user uses two-factor-authentication
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
Team invite resource
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
The team invite resource contains the following public fields:
|
||||||
|
|
||||||
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
Field Type Description
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
id integer Internal ID of the invite
|
||||||
|
email string The invitee's email address
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
Team API token resource
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
The team API token resource contains the following public fields:
|
||||||
|
|
||||||
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
Field Type Description
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
id integer Internal ID of the invite
|
||||||
|
name string Name of this API token
|
||||||
|
active boolean Whether this API token is active (can never be set to
|
||||||
|
``true`` again once ``false``)
|
||||||
|
token string The actual API token. Will only be sent back during
|
||||||
|
token creation.
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
Team endpoints
|
||||||
|
--------------
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/teams/
|
||||||
|
|
||||||
|
Returns a list of all teams within a given organizer.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/teams/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Admin team",
|
||||||
|
"all_events": true,
|
||||||
|
"limit_events": [],
|
||||||
|
"can_create_events": true,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/teams/(id)/
|
||||||
|
|
||||||
|
Returns information on one team, identified by its ID.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/teams/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,
|
||||||
|
"name": "Admin team",
|
||||||
|
"all_events": true,
|
||||||
|
"limit_events": [],
|
||||||
|
"can_create_events": true,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param id: The ``id`` field of the team to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/teams/
|
||||||
|
|
||||||
|
Creates a new team
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/teams/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Admin team",
|
||||||
|
"all_events": true,
|
||||||
|
"limit_events": [],
|
||||||
|
"can_create_events": true,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "Admin team",
|
||||||
|
"all_events": true,
|
||||||
|
"limit_events": [],
|
||||||
|
"can_create_events": true,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to create a team for
|
||||||
|
:statuscode 201: no error
|
||||||
|
:statuscode 400: The team could not be created due to invalid submitted data.
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
|
||||||
|
|
||||||
|
.. http:patch:: /api/v1/organizers/(organizer)/teams/(id)/
|
||||||
|
|
||||||
|
Update a team. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
|
||||||
|
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
|
||||||
|
want to change.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
PATCH /api/v1/organizers/bigevents/teams/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
Content-Length: 94
|
||||||
|
|
||||||
|
{
|
||||||
|
"can_create_events": true
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Admin team",
|
||||||
|
"all_events": true,
|
||||||
|
"limit_events": [],
|
||||||
|
"can_create_events": true,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param id: The ``id`` field of the team to modify
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 400: The team could not be modified due to invalid submitted data
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource.
|
||||||
|
|
||||||
|
.. http:delete:: /api/v1/organizers/(organizer)/teams/(id)/
|
||||||
|
|
||||||
|
Deletes a team.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
DELETE /api/v1/organizers/bigevents/teams/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 204 No Content
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param id: The ``id`` field of the team to delete
|
||||||
|
:statuscode 204: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource.
|
||||||
|
|
||||||
|
Team member endpoints
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/teams/(team)/members/
|
||||||
|
|
||||||
|
Returns a list of all members of a team.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/teams/1/members/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"fullname": "John Doe",
|
||||||
|
"email": "john@example.com",
|
||||||
|
"require_2fa": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param team: The ``id`` field of the team to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||||
|
:statuscode 404: The requested team does not exist
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/teams/(team)/members/(id)/
|
||||||
|
|
||||||
|
Returns information on one team member, identified by their ID.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/teams/1/members/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,
|
||||||
|
"fullname": "John Doe",
|
||||||
|
"email": "john@example.com",
|
||||||
|
"require_2fa": true
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param team: The ``id`` field of the team to fetch
|
||||||
|
:param id: The ``id`` field of the member to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||||
|
:statuscode 404: The requested team or member does not exist
|
||||||
|
|
||||||
|
.. http:delete:: /api/v1/organizers/(organizer)/teams/(team)/members/(id)/
|
||||||
|
|
||||||
|
Removes a member from the team.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
DELETE /api/v1/organizers/bigevents/teams/1/members/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 204 No Content
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param team: The ``id`` field of the team to modify
|
||||||
|
:param id: The ``id`` field of the member to delete
|
||||||
|
:statuscode 204: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
|
||||||
|
:statuscode 404: The requested team or member does not exist
|
||||||
|
|
||||||
|
Team invite endpoints
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/teams/(team)/invites/
|
||||||
|
|
||||||
|
Returns a list of all invitations to a team.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/teams/1/invites/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"email": "john@example.com"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param team: The ``id`` field of the team to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||||
|
:statuscode 404: The requested team does not exist
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/teams/(team)/invites/(id)/
|
||||||
|
|
||||||
|
Returns information on one invite, identified by its ID.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/teams/1/invites/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,
|
||||||
|
"email": "john@example.org"
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param team: The ``id`` field of the team to fetch
|
||||||
|
:param id: The ``id`` field of the invite to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||||
|
:statuscode 404: The requested team or invite does not exist
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/teams/(team)/invites/
|
||||||
|
|
||||||
|
Invites someone into the team. Note that if the user already has a pretix account, you will receive a response without
|
||||||
|
an ``id`` and instead of an invite being created, the user will be directly added to the team.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/teams/1/invites/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
Content-Length: 94
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "mark@example.org"
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"email": "mark@example.org"
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param team: The ``id`` field of the team to modify
|
||||||
|
:statuscode 204: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
|
||||||
|
:statuscode 404: The requested team does not exist
|
||||||
|
|
||||||
|
.. http:delete:: /api/v1/organizers/(organizer)/teams/(team)/invites/(id)/
|
||||||
|
|
||||||
|
Revokes an invite.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
DELETE /api/v1/organizers/bigevents/teams/1/invites/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 204 No Content
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param team: The ``id`` field of the team to modify
|
||||||
|
:param id: The ``id`` field of the invite to delete
|
||||||
|
:statuscode 204: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
|
||||||
|
:statuscode 404: The requested team or invite does not exist
|
||||||
|
|
||||||
|
Team API token endpoints
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/teams/(team)/tokens/
|
||||||
|
|
||||||
|
Returns a list of all API tokens of a team.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/teams/1/tokens/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"active": true,
|
||||||
|
"name": "Test token"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param team: The ``id`` field of the team to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||||
|
:statuscode 404: The requested team does not exist
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/teams/(team)/tokens/(id)/
|
||||||
|
|
||||||
|
Returns information on one token, identified by its ID.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/teams/1/tokens/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,
|
||||||
|
"active": true,
|
||||||
|
"name": "Test token"
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param team: The ``id`` field of the team to fetch
|
||||||
|
:param id: The ``id`` field of the token to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||||
|
:statuscode 404: The requested team or token does not exist
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/teams/(team)/tokens/
|
||||||
|
|
||||||
|
Creates a new token.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/teams/1/tokens/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
Content-Length: 94
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "New token"
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "New token",
|
||||||
|
"active": true,
|
||||||
|
"token": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param team: The ``id`` field of the team to create a token for
|
||||||
|
:statuscode 204: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
|
||||||
|
:statuscode 404: The requested team does not exist
|
||||||
|
|
||||||
|
.. http:delete:: /api/v1/organizers/(organizer)/teams/(team)/tokens/(id)/
|
||||||
|
|
||||||
|
Disables a token.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
DELETE /api/v1/organizers/bigevents/teams/1/tokens/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "My token",
|
||||||
|
"active": false
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param team: The ``id`` field of the team to modify
|
||||||
|
:param id: The ``id`` field of the token to delete
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
|
||||||
|
:statuscode 404: The requested team or token does not exist
|
||||||
@@ -66,7 +66,7 @@ event-related views, there is also a signal that allows you to add the view to t
|
|||||||
|
|
||||||
from django.urls import resolve, reverse
|
from django.urls import resolve, reverse
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from pretix.control.signals import nav_event
|
from pretix.control.signals import nav_event
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,17 +20,24 @@ Order events
|
|||||||
There are multiple signals that will be sent out in the ordering cycle:
|
There are multiple signals that will be sent out in the ordering cycle:
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
:members: validate_cart, validate_cart_addons, validate_order, order_fee_calculation, order_paid, order_placed, order_canceled, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text
|
:members: validate_cart, validate_cart_addons, validate_order, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text
|
||||||
|
|
||||||
|
Check-ins
|
||||||
|
"""""""""
|
||||||
|
|
||||||
|
.. automodule:: pretix.base.signals
|
||||||
|
:members: checkin_created
|
||||||
|
|
||||||
|
|
||||||
Frontend
|
Frontend
|
||||||
--------
|
--------
|
||||||
|
|
||||||
.. automodule:: pretix.presale.signals
|
.. automodule:: pretix.presale.signals
|
||||||
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, render_seating_plan, checkout_flow_steps, position_info, item_description
|
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: pretix.presale.signals
|
.. automodule:: pretix.presale.signals
|
||||||
:members: order_info, order_meta_from_request
|
:members: order_info, order_info_top, order_meta_from_request
|
||||||
|
|
||||||
Request flow
|
Request flow
|
||||||
""""""""""""
|
""""""""""""
|
||||||
@@ -81,3 +88,9 @@ Ticket designs
|
|||||||
|
|
||||||
.. automodule:: pretix.plugins.ticketoutputpdf.signals
|
.. automodule:: pretix.plugins.ticketoutputpdf.signals
|
||||||
:members: override_layout
|
:members: override_layout
|
||||||
|
|
||||||
|
API
|
||||||
|
---
|
||||||
|
|
||||||
|
.. automodule:: pretix.base.signals
|
||||||
|
:members: validate_event_settings, api_event_settings_fields
|
||||||
|
|||||||
@@ -114,6 +114,8 @@ The provider class
|
|||||||
|
|
||||||
.. automethod:: api_payment_details
|
.. automethod:: api_payment_details
|
||||||
|
|
||||||
|
.. automethod:: matching_id
|
||||||
|
|
||||||
.. automethod:: shred_payment_info
|
.. automethod:: shred_payment_info
|
||||||
|
|
||||||
.. automethod:: cancel_payment
|
.. automethod:: cancel_payment
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ name string The human-readable name of your plugin
|
|||||||
author string Your name
|
author string Your name
|
||||||
version string A human-readable version code of your plugin
|
version string A human-readable version code of your plugin
|
||||||
description string A more verbose description of what your plugin does.
|
description string A more verbose description of what your plugin does.
|
||||||
|
category string Category of a plugin. Either one of ``"FEATURE"``, ``"PAYMENT"``,
|
||||||
|
``"INTEGRATION"``, ``"CUSTOMIZATION"``, ``"FORMAT"``, or ``"API"``,
|
||||||
|
or any other string.
|
||||||
visible boolean (optional) ``True`` by default, can hide a plugin so it cannot be normally activated.
|
visible boolean (optional) ``True`` by default, can hide a plugin so it cannot be normally activated.
|
||||||
restricted boolean (optional) ``False`` by default, restricts a plugin such that it can only be enabled
|
restricted boolean (optional) ``False`` by default, restricts a plugin such that it can only be enabled
|
||||||
for an event by system administrators / superusers.
|
for an event by system administrators / superusers.
|
||||||
@@ -58,7 +61,7 @@ A working example would be::
|
|||||||
from pretix.base.plugins import PluginConfig
|
from pretix.base.plugins import PluginConfig
|
||||||
except ImportError:
|
except ImportError:
|
||||||
raise RuntimeError("Please use pretix 2.7 or above to run this plugin!")
|
raise RuntimeError("Please use pretix 2.7 or above to run this plugin!")
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class PaypalApp(PluginConfig):
|
class PaypalApp(PluginConfig):
|
||||||
@@ -69,6 +72,7 @@ A working example would be::
|
|||||||
name = _("PayPal")
|
name = _("PayPal")
|
||||||
author = _("the pretix team")
|
author = _("the pretix team")
|
||||||
version = '1.0.0'
|
version = '1.0.0'
|
||||||
|
category = 'PAYMENT
|
||||||
visible = True
|
visible = True
|
||||||
restricted = False
|
restricted = False
|
||||||
description = _("This plugin allows you to receive payments via PayPal")
|
description = _("This plugin allows you to receive payments via PayPal")
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ We now need a way to translate the action codes like ``pretix.event.changed`` in
|
|||||||
strings. The :py:attr:`pretix.base.signals.logentry_display` signals allows you to do so. A simple
|
strings. The :py:attr:`pretix.base.signals.logentry_display` signals allows you to do so. A simple
|
||||||
implementation could look like::
|
implementation could look like::
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
from pretix.base.signals import logentry_display
|
from pretix.base.signals import logentry_display
|
||||||
|
|
||||||
@receiver(signal=logentry_display)
|
@receiver(signal=logentry_display)
|
||||||
|
|||||||
224
doc/plugins/campaigns.rst
Normal file
224
doc/plugins/campaigns.rst
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
Campaigns
|
||||||
|
=========
|
||||||
|
|
||||||
|
The campaigns plugin provides a HTTP API that allows you to create new campaigns.
|
||||||
|
|
||||||
|
Resource description
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
The campaign resource contains the following public fields:
|
||||||
|
|
||||||
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
Field Type Description
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
id integer Internal campaign ID
|
||||||
|
code string The URL component of the campaign, e.g. with code ``BAR``
|
||||||
|
the campaign URL would to be ``https://<server>/<organizer>/<event>/c/BAR/``.
|
||||||
|
This value needs to be *globally unique* and we do not
|
||||||
|
recommend setting it manually. If you omit it, a random
|
||||||
|
value will be chosen.
|
||||||
|
description string An internal, human-readable name of the campaign.
|
||||||
|
external_target string An URL to redirect to from the tracking link. To redirect to
|
||||||
|
the ticket shop, use an empty string.
|
||||||
|
order_count integer Number of orders tracked on this campaign (read-only)
|
||||||
|
click_count integer Number of clicks tracked on this campaign (read-only)
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
Endpoints
|
||||||
|
---------
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/campaigns/
|
||||||
|
|
||||||
|
Returns a list of all campaigns configured for an event.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/campaigns/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"code": "wZnL11fjq",
|
||||||
|
"description": "Facebook",
|
||||||
|
"external_target": "",
|
||||||
|
"order_count:" 0,
|
||||||
|
"click_count:" 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
: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 the event to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/campaigns/(id)/
|
||||||
|
|
||||||
|
Returns information on one campaign, identified by its ID.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/campaigns/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,
|
||||||
|
"code": "wZnL11fjq",
|
||||||
|
"description": "Facebook",
|
||||||
|
"external_target": "",
|
||||||
|
"order_count:" 0,
|
||||||
|
"click_count:" 0
|
||||||
|
}
|
||||||
|
|
||||||
|
: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 campaign to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event/campaign does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/campaigns/
|
||||||
|
|
||||||
|
Create a new campaign.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/campaigns/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
Content-Length: 166
|
||||||
|
|
||||||
|
{
|
||||||
|
"description": "Twitter"
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"code": "IfVJQzSBL",
|
||||||
|
"description": "Twitter",
|
||||||
|
"external_target": "",
|
||||||
|
"order_count:" 0,
|
||||||
|
"click_count:" 0
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to create a campaign for
|
||||||
|
:param event: The ``slug`` field of the event to create a campaign for
|
||||||
|
:statuscode 201: no error
|
||||||
|
:statuscode 400: The campaign could not be created due to invalid submitted data.
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create campaigns.
|
||||||
|
|
||||||
|
|
||||||
|
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/campaigns/(id)/
|
||||||
|
|
||||||
|
Update a campaign. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
|
||||||
|
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
|
||||||
|
want to change.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
PATCH /api/v1/organizers/bigevents/events/sampleconf/campaigns/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
Content-Length: 34
|
||||||
|
|
||||||
|
{
|
||||||
|
"external_target": "https://mywebsite.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: text/javascript
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"code": "IfVJQzSBL",
|
||||||
|
"description": "Twitter",
|
||||||
|
"external_target": "https://mywebsite.com",
|
||||||
|
"order_count:" 0,
|
||||||
|
"click_count:" 0
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param event: The ``slug`` field of the event to modify
|
||||||
|
:param id: The ``id`` field of the campaign to modify
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 400: The campaign could not be modified due to invalid submitted data.
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event/campaign does not exist **or** you have no permission to change it.
|
||||||
|
|
||||||
|
|
||||||
|
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/campaigns/(id)/
|
||||||
|
|
||||||
|
Delete a campaign and all associated data.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
DELETE /api/v1/organizers/bigevents/events/sampleconf/campaigns/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 204 No Content
|
||||||
|
Vary: Accept
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param event: The ``slug`` field of the event to modify
|
||||||
|
:param id: The ``id`` field of the campaign to delete
|
||||||
|
:statuscode 204: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event/campaign does not exist **or** you have no permission to change it
|
||||||
277
doc/plugins/digital.rst
Normal file
277
doc/plugins/digital.rst
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
Digital content
|
||||||
|
===============
|
||||||
|
|
||||||
|
The digital content plugin provides a HTTP API that allows you to create new digital content for your ticket holders,
|
||||||
|
such as live streams, videos, or material downloads.
|
||||||
|
|
||||||
|
Resource description
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
The digital content resource contains the following public fields:
|
||||||
|
|
||||||
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
Field Type Description
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
id integer Internal content ID
|
||||||
|
title multi-lingual string The content title (required)
|
||||||
|
content_type string The type of content, valid values are ``webinar``, ``video``, ``livestream``, ``link``, ``file``
|
||||||
|
url string The location of the digital content
|
||||||
|
description multi-lingual string A public description of the item. May contain Markdown
|
||||||
|
syntax and is not required.
|
||||||
|
available_from datetime The first date time at which this content will be shown
|
||||||
|
(or ``null``).
|
||||||
|
available_until datetime The last date time at which this content will b e shown
|
||||||
|
(or ``null``).
|
||||||
|
all_products boolean If ``true``, the content is available to all buyers of tickets for this event. The ``limit_products`` field is ignored in this case.
|
||||||
|
limit_products list of integers List of product/item IDs. This content is only shown to buyers of these ticket types.
|
||||||
|
position integer An integer, used for sorting
|
||||||
|
subevent integer Date in an event series this content should be shown for. Should be ``null`` if this is not an event series or if this should be shown to all customers.
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
Endpoints
|
||||||
|
---------
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/
|
||||||
|
|
||||||
|
Returns a list of all digital content configured for an event.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"subevent": null,
|
||||||
|
"title": {
|
||||||
|
"en": "Concert livestream"
|
||||||
|
},
|
||||||
|
"content_type": "link",
|
||||||
|
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||||
|
"description": {
|
||||||
|
"en": "Watch our event live here on YouTube!"
|
||||||
|
},
|
||||||
|
"all_products": true,
|
||||||
|
"limit_products": [],
|
||||||
|
"available_from": "2020-03-22T23:00:00Z",
|
||||||
|
"available_until": null,
|
||||||
|
"position": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
: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 the event to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/(id)/
|
||||||
|
|
||||||
|
Returns information on one content item, identified by its ID.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/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,
|
||||||
|
"subevent": null,
|
||||||
|
"title": {
|
||||||
|
"en": "Concert livestream"
|
||||||
|
},
|
||||||
|
"content_type": "link",
|
||||||
|
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||||
|
"description": {
|
||||||
|
"en": "Watch our event live here on YouTube!"
|
||||||
|
},
|
||||||
|
"all_products": true,
|
||||||
|
"limit_products": [],
|
||||||
|
"available_from": "2020-03-22T23:00:00Z",
|
||||||
|
"available_until": null,
|
||||||
|
"position": 1
|
||||||
|
}
|
||||||
|
|
||||||
|
: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 content to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event/content does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/
|
||||||
|
|
||||||
|
Create a new digital content.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
Content-Length: 166
|
||||||
|
|
||||||
|
{
|
||||||
|
"subevent": null,
|
||||||
|
"title": {
|
||||||
|
"en": "Concert livestream"
|
||||||
|
},
|
||||||
|
"content_type": "link",
|
||||||
|
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||||
|
"description": {
|
||||||
|
"en": "Watch our event live here on YouTube!"
|
||||||
|
},
|
||||||
|
"all_products": true,
|
||||||
|
"limit_products": [],
|
||||||
|
"available_from": "2020-03-22T23:00:00Z",
|
||||||
|
"available_until": null,
|
||||||
|
"position": 1
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"subevent": null,
|
||||||
|
"title": {
|
||||||
|
"en": "Concert livestream"
|
||||||
|
},
|
||||||
|
"content_type": "link",
|
||||||
|
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||||
|
"description": {
|
||||||
|
"en": "Watch our event live here on YouTube!"
|
||||||
|
},
|
||||||
|
"all_products": true,
|
||||||
|
"limit_products": [],
|
||||||
|
"available_from": "2020-03-22T23:00:00Z",
|
||||||
|
"available_until": null,
|
||||||
|
"position": 1
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to create new content for
|
||||||
|
:param event: The ``slug`` field of the event to create new content for
|
||||||
|
:statuscode 201: no error
|
||||||
|
:statuscode 400: The content could not be created due to invalid submitted data.
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create digital contents.
|
||||||
|
|
||||||
|
|
||||||
|
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/(id)/
|
||||||
|
|
||||||
|
Update a content. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
|
||||||
|
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
|
||||||
|
want to change.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
PATCH /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
Content-Length: 34
|
||||||
|
|
||||||
|
{
|
||||||
|
"url": "https://mywebsite.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: text/javascript
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"subevent": null,
|
||||||
|
"title": {
|
||||||
|
"en": "Concert livestream"
|
||||||
|
},
|
||||||
|
"content_type": "link",
|
||||||
|
"url": "https://mywebsite.com",
|
||||||
|
"description": {
|
||||||
|
"en": "Watch our event live here on YouTube!"
|
||||||
|
},
|
||||||
|
"all_products": true,
|
||||||
|
"limit_products": [],
|
||||||
|
"available_from": "2020-03-22T23:00:00Z",
|
||||||
|
"available_until": null,
|
||||||
|
"position": 1
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param event: The ``slug`` field of the event to modify
|
||||||
|
:param id: The ``id`` field of the content to modify
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 400: The content could not be modified due to invalid submitted data.
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event/content does not exist **or** you have no permission to change it.
|
||||||
|
|
||||||
|
|
||||||
|
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/(id)/
|
||||||
|
|
||||||
|
Delete a digital content.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
DELETE /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 204 No Content
|
||||||
|
Vary: Accept
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param event: The ``slug`` field of the event to modify
|
||||||
|
:param id: The ``id`` field of the content to delete
|
||||||
|
:statuscode 204: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event/content does not exist **or** you have no permission to change it
|
||||||
@@ -14,3 +14,5 @@ If you want to **create** a plugin, please go to the
|
|||||||
banktransfer
|
banktransfer
|
||||||
ticketoutputpdf
|
ticketoutputpdf
|
||||||
badges
|
badges
|
||||||
|
campaigns
|
||||||
|
digital
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
-r ../src/requirements.txt
|
-r ../src/requirements.txt
|
||||||
sphinx==1.6.*
|
sphinx==2.3.*
|
||||||
sphinx-rtd-theme
|
sphinx-rtd-theme
|
||||||
sphinxcontrib-httpdomain
|
sphinxcontrib-httpdomain
|
||||||
sphinxcontrib-images
|
sphinxcontrib-images
|
||||||
sphinxcontrib-spelling
|
sphinxcontrib-spelling
|
||||||
|
pygments-markdown-lexer
|
||||||
# See https://github.com/rfk/pyenchant/pull/130
|
# See https://github.com/rfk/pyenchant/pull/130
|
||||||
git+https://github.com/raphaelm/pyenchant.git@patch-1#egg=pyenchant
|
git+https://github.com/raphaelm/pyenchant.git@patch-1#egg=pyenchant
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ regex
|
|||||||
renderer
|
renderer
|
||||||
renderers
|
renderers
|
||||||
reportlab
|
reportlab
|
||||||
|
reseller
|
||||||
SaaS
|
SaaS
|
||||||
scalability
|
scalability
|
||||||
screenshot
|
screenshot
|
||||||
@@ -110,9 +111,10 @@ scss
|
|||||||
searchable
|
searchable
|
||||||
selectable
|
selectable
|
||||||
serializable
|
serializable
|
||||||
serializers
|
serializer
|
||||||
serializers
|
serializers
|
||||||
sexualized
|
sexualized
|
||||||
|
SQL
|
||||||
startup
|
startup
|
||||||
stdout
|
stdout
|
||||||
stylesheet
|
stylesheet
|
||||||
@@ -139,6 +141,7 @@ untrusted
|
|||||||
uptime
|
uptime
|
||||||
username
|
username
|
||||||
url
|
url
|
||||||
|
validator
|
||||||
versa
|
versa
|
||||||
versioning
|
versioning
|
||||||
viewable
|
viewable
|
||||||
|
|||||||
@@ -114,6 +114,17 @@ If you want to disable voucher input in the widget, you can pass the ``disable-v
|
|||||||
|
|
||||||
<pretix-widget event="https://pretix.eu/demo/democon/" disable-vouchers></pretix-widget>
|
<pretix-widget event="https://pretix.eu/demo/democon/" disable-vouchers></pretix-widget>
|
||||||
|
|
||||||
|
Filtering products
|
||||||
|
------------------
|
||||||
|
|
||||||
|
You can filter the products shown in the widget by passing in a list of product IDs::
|
||||||
|
|
||||||
|
<pretix-widget event="https://pretix.eu/demo/democon/" items="23,42"></pretix-widget>
|
||||||
|
|
||||||
|
Alternatively, you can select one or more categories to be shown::
|
||||||
|
|
||||||
|
<pretix-widget event="https://pretix.eu/demo/democon/" categories="12,25"></pretix-widget>
|
||||||
|
|
||||||
Multi-event selection
|
Multi-event selection
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
@@ -183,6 +194,24 @@ Just as the widget, the button supports the optional attributes ``voucher`` and
|
|||||||
|
|
||||||
You can style the button using the ``pretix-button`` CSS class.
|
You can style the button using the ``pretix-button`` CSS class.
|
||||||
|
|
||||||
|
Dynamically opening the widget
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
You can get the behavior of the pretix Button without a button at all, so you can trigger it from your own code in
|
||||||
|
response to a user action. Usually, this will open an overlay with your ticket shop, however in some cases, such as
|
||||||
|
missing HTTPS encryption on your case or a really small screen (mobile), it will open a new tab instead of an overlay.
|
||||||
|
Therefore, make sure you call this *in direct response to a user action*, otherwise most browser will block it as an
|
||||||
|
unwanted pop-up.
|
||||||
|
|
||||||
|
.. js:function:: window.PretixWidget.open(target_url [, voucher [, subevent [, items, [, widget_data [, skip_ssl_check ]]]]])
|
||||||
|
|
||||||
|
:param string target_url: The URL of the ticket shop.
|
||||||
|
:param string voucher: A voucher code to be pre-selected, or ``null``.
|
||||||
|
:param string subevent: A subevent to be pre-selected, or ``null``.
|
||||||
|
:param array items: A collection of items to be put in the cart, of the form ``[{"item": "item_3", "count": 1}, {"item": "variation_5_6", "count": 4}]``
|
||||||
|
:param object widget_data: Additional data to be passed to the shop, see below.
|
||||||
|
:param boolean skip_ssl_check: Whether to ignore the check for HTTPS. Only to be used during development.
|
||||||
|
|
||||||
Dynamically loading the widget
|
Dynamically loading the widget
|
||||||
------------------------------
|
------------------------------
|
||||||
|
|
||||||
@@ -238,7 +267,8 @@ with that information::
|
|||||||
data-question-L9G8NG9M="Foobar">
|
data-question-L9G8NG9M="Foobar">
|
||||||
</pretix-widget>
|
</pretix-widget>
|
||||||
|
|
||||||
This works for the pretix Button as well. Currently, the following attributes are understood by pretix itself:
|
This works for the pretix Button as well, if you also specify a product.
|
||||||
|
Currently, the following attributes are understood by pretix itself:
|
||||||
|
|
||||||
* ``data-email`` will pre-fill the order email field as well as the attendee email field (if enabled).
|
* ``data-email`` will pre-fill the order email field as well as the attendee email field (if enabled).
|
||||||
|
|
||||||
@@ -303,4 +333,8 @@ Hosted or pretix Enterprise are active, you can pass the following fields:
|
|||||||
Data passing options have been added in pretix 2.3. If you use a self-hosted version of pretix, they only work
|
Data passing options have been added in pretix 2.3. If you use a self-hosted version of pretix, they only work
|
||||||
fully if you configured a redis server.
|
fully if you configured a redis server.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.6
|
||||||
|
|
||||||
|
Dynamically opening the widget has been added in pretix 3.6.
|
||||||
|
|
||||||
.. _Let's Encrypt: https://letsencrypt.org/
|
.. _Let's Encrypt: https://letsencrypt.org/
|
||||||
|
|||||||
@@ -40,6 +40,24 @@ If you created a product and it doesn't show up, please follow the following ste
|
|||||||
6. If the sale period has not started yet or is already over, check the "Show items outside presale period" setting of
|
6. If the sale period has not started yet or is already over, check the "Show items outside presale period" setting of
|
||||||
your event.
|
your event.
|
||||||
|
|
||||||
|
Can I have different payment deadlines for different payment methods?
|
||||||
|
---------------------------------------------------------------------
|
||||||
|
|
||||||
|
No. We do not think it makes a lot of sense, for a number of reasons. First of all we believe it is not very
|
||||||
|
customer-friendly. You might for example want to configure a 1-day deadline for credit card payments and 2 weeks for
|
||||||
|
bank transfers. However, think for example of a customer who wants to pay by card and then the payment fails because
|
||||||
|
the bank locked the card or refused the payment. The customer now needs to worry about not getting their ticket, or
|
||||||
|
needs to create a new order with a different payment method. A payment deadline is a guarantee to your customer to hold
|
||||||
|
the ticket if it is paid for within a certain time frame. If you give a two-week guarantee to some of your customers,
|
||||||
|
why not to others?
|
||||||
|
|
||||||
|
There are some other issues with it as well. pretix allows customers to switch payment methods as long as their payment
|
||||||
|
has not been started or if it has failed. For example, a customer who selected bank transfer can later switch to credit
|
||||||
|
card if they haven't sent the money yet, or a customer with a failed credit card payment can switch to a different
|
||||||
|
method without creating a new order. If payment deadlines were dependent on the payment method, switching back and
|
||||||
|
forth could either allow someone to extend their deadline forever, or render someones order invalid by moving the date
|
||||||
|
back in the past.
|
||||||
|
|
||||||
How can I revert a check-in?
|
How can I revert a check-in?
|
||||||
----------------------------
|
----------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -14,30 +14,23 @@ and with pretix, you can do this. On this page, you find out the necessary steps
|
|||||||
With the pretix.eu hosted service
|
With the pretix.eu hosted service
|
||||||
---------------------------------
|
---------------------------------
|
||||||
|
|
||||||
Step 1: DNS Configuration
|
Go to "Organizers" in the backend and select your organizer account. Then, go to "Settings" and "Custom Domain".
|
||||||
#########################
|
|
||||||
|
This page will show you instructions on how to set up your own domain. Basically, it works like this:
|
||||||
|
|
||||||
Go to the website of the provider you registered your domain name with. Look for the "DNS" settings page in their
|
Go to the website of the provider you registered your domain name with. Look for the "DNS" settings page in their
|
||||||
interface. Unfortunately, we can't tell you exactly how that is named and how it looks, since it is different for every
|
interface. Unfortunately, we can't tell you exactly how that is named and how it looks, since it is different for every
|
||||||
domain provider.
|
domain provider.
|
||||||
|
|
||||||
Use this interface to add a new subdomain record, e.g. ``tickets`` of the type ``CNAME`` (might also be called "alias").
|
Use this interface to add a new subdomain record, e.g. ``tickets`` of the type ``CNAME`` (might also be called "alias").
|
||||||
The value of the record should be ``www.pretix.eu``.
|
The value of the record should be the one shown on the "Custom Domain" page in pretix' backend.
|
||||||
|
|
||||||
Step 2: Wait for the DNS entry to propagate
|
|
||||||
###########################################
|
|
||||||
|
|
||||||
Submit your changes and wait a bit, it can regularly take up to three hours for DNS changes to propagate to the caches
|
Submit your changes and wait a bit, it can regularly take up to three hours for DNS changes to propagate to the caches
|
||||||
of all DNS servers. You can try checking by accessing your new subdomain, ``http://tickets.awesomepartycorp.com``.
|
of all DNS servers. You can try checking by accessing your new subdomain, ``http://tickets.awesomepartycorp.com``.
|
||||||
If DNS was changed successfully, you should see a SSL certificate error. If you ignore the error and access the page
|
If DNS was changed successfully, you should see a SSL certificate error. If you ignore the error and access the page
|
||||||
anyways, you should get a pretix-themed error page with the headline "Unknown domain".
|
anyways, you should get a pretix-themed error page with the headline "Unknown domain".
|
||||||
|
|
||||||
Step 3: Tell us
|
Now, tell us about your domain on the "Custom Domain" page to get started.
|
||||||
###############
|
|
||||||
|
|
||||||
Write an email to support@pretix.eu, naming your new domain and your organizer account. We will then generate a SSL
|
|
||||||
certificate for you (for free!) and configure the domain.
|
|
||||||
|
|
||||||
|
|
||||||
With a custom pretix installation
|
With a custom pretix installation
|
||||||
---------------------------------
|
---------------------------------
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "3.5.0"
|
__version__ = "3.8.0"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from datetime import timedelta
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from oauth2_provider.generators import (
|
from oauth2_provider.generators import (
|
||||||
generate_client_id, generate_client_secret,
|
generate_client_id, generate_client_secret,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from datetime import timedelta
|
|||||||
|
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext_lazy
|
from django.utils.translation import gettext_lazy
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
|||||||
else validated_data.get('item').quotas.filter(subevent=validated_data.get('subevent')))
|
else validated_data.get('item').quotas.filter(subevent=validated_data.get('subevent')))
|
||||||
if len(new_quotas) == 0:
|
if len(new_quotas) == 0:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
ugettext_lazy('The product "{}" is not assigned to a quota.').format(
|
gettext_lazy('The product "{}" is not assigned to a quota.').format(
|
||||||
str(validated_data.get('item'))
|
str(validated_data.get('item'))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -64,8 +64,8 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
|||||||
avail = quota.availability()
|
avail = quota.availability()
|
||||||
if avail[0] != Quota.AVAILABILITY_OK or (avail[1] is not None and avail[1] < 1):
|
if avail[0] != Quota.AVAILABILITY_OK or (avail[1] is not None and avail[1] < 1):
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
ugettext_lazy('There is not enough quota available on quota "{}" to perform '
|
gettext_lazy('There is not enough quota available on quota "{}" to perform '
|
||||||
'the operation.').format(
|
'the operation.').format(
|
||||||
quota.name
|
quota.name
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -88,7 +88,7 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
|||||||
else:
|
else:
|
||||||
validated_data['seat'] = seat
|
validated_data['seat'] = seat
|
||||||
if not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web')):
|
if not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web')):
|
||||||
raise ValidationError(ugettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name))
|
raise ValidationError(gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name))
|
||||||
elif seated:
|
elif seated:
|
||||||
raise ValidationError('The specified product requires to choose a seat.')
|
raise ValidationError('The specified product requires to choose a seat.')
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ from django.conf import settings
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django_countries.serializers import CountryFieldMixin
|
from django_countries.serializers import CountryFieldMixin
|
||||||
|
from hierarkey.proxy import HierarkeyProxy
|
||||||
from pytz import common_timezones
|
from pytz import common_timezones
|
||||||
|
from rest_framework import serializers
|
||||||
from rest_framework.fields import ChoiceField, Field
|
from rest_framework.fields import ChoiceField, Field
|
||||||
from rest_framework.relations import SlugRelatedField
|
from rest_framework.relations import SlugRelatedField
|
||||||
|
|
||||||
@@ -15,6 +17,8 @@ from pretix.base.models.items import SubEventItem, SubEventItemVariation
|
|||||||
from pretix.base.services.seating import (
|
from pretix.base.services.seating import (
|
||||||
SeatProtected, generate_seats, validate_plan_change,
|
SeatProtected, generate_seats, validate_plan_change,
|
||||||
)
|
)
|
||||||
|
from pretix.base.settings import DEFAULTS, validate_settings
|
||||||
|
from pretix.base.signals import api_event_settings_fields
|
||||||
|
|
||||||
|
|
||||||
class MetaDataField(Field):
|
class MetaDataField(Field):
|
||||||
@@ -30,6 +34,19 @@ class MetaDataField(Field):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MetaPropertyField(Field):
|
||||||
|
|
||||||
|
def to_representation(self, value):
|
||||||
|
return {
|
||||||
|
v.name: v.default for v in value.item_meta_properties.all()
|
||||||
|
}
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
return {
|
||||||
|
'item_meta_properties': data
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class SeatCategoryMappingField(Field):
|
class SeatCategoryMappingField(Field):
|
||||||
|
|
||||||
def to_representation(self, value):
|
def to_representation(self, value):
|
||||||
@@ -73,6 +90,7 @@ class TimeZoneField(ChoiceField):
|
|||||||
|
|
||||||
class EventSerializer(I18nAwareModelSerializer):
|
class EventSerializer(I18nAwareModelSerializer):
|
||||||
meta_data = MetaDataField(required=False, source='*')
|
meta_data = MetaDataField(required=False, source='*')
|
||||||
|
item_meta_properties = MetaPropertyField(required=False, source='*')
|
||||||
plugins = PluginsField(required=False, source='*')
|
plugins = PluginsField(required=False, source='*')
|
||||||
seat_category_mapping = SeatCategoryMappingField(source='*', required=False)
|
seat_category_mapping = SeatCategoryMappingField(source='*', required=False)
|
||||||
timezone = TimeZoneField(required=False, choices=[(a, a) for a in common_timezones])
|
timezone = TimeZoneField(required=False, choices=[(a, a) for a in common_timezones])
|
||||||
@@ -82,7 +100,7 @@ class EventSerializer(I18nAwareModelSerializer):
|
|||||||
fields = ('name', 'slug', 'live', 'testmode', 'currency', 'date_from',
|
fields = ('name', 'slug', 'live', 'testmode', 'currency', 'date_from',
|
||||||
'date_to', 'date_admission', 'is_public', 'presale_start',
|
'date_to', 'date_admission', 'is_public', 'presale_start',
|
||||||
'presale_end', 'location', 'geo_lat', 'geo_lon', 'has_subevents', 'meta_data', 'seating_plan',
|
'presale_end', 'location', 'geo_lat', 'geo_lon', 'has_subevents', 'meta_data', 'seating_plan',
|
||||||
'plugins', 'seat_category_mapping', 'timezone')
|
'plugins', 'seat_category_mapping', 'timezone', 'item_meta_properties')
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
data = super().validate(data)
|
data = super().validate(data)
|
||||||
@@ -127,6 +145,12 @@ class EventSerializer(I18nAwareModelSerializer):
|
|||||||
raise ValidationError(_('Meta data property \'{name}\' does not exist.').format(name=key))
|
raise ValidationError(_('Meta data property \'{name}\' does not exist.').format(name=key))
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def item_meta_props(self):
|
||||||
|
return {
|
||||||
|
p.name: p for p in self.context['request'].event.item_meta_properties.all()
|
||||||
|
}
|
||||||
|
|
||||||
def validate_seating_plan(self, value):
|
def validate_seating_plan(self, value):
|
||||||
if value and value.organizer != self.context['request'].organizer:
|
if value and value.organizer != self.context['request'].organizer:
|
||||||
raise ValidationError('Invalid seating plan.')
|
raise ValidationError('Invalid seating plan.')
|
||||||
@@ -138,8 +162,11 @@ class EventSerializer(I18nAwareModelSerializer):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
def validate_seat_category_mapping(self, value):
|
def validate_seat_category_mapping(self, value):
|
||||||
if value and value['seat_category_mapping'] and (not self.instance or not self.instance.pk):
|
if not self.instance or not self.instance.pk:
|
||||||
raise ValidationError('You cannot specify seat category mappings on event creation.')
|
if value and value['seat_category_mapping']:
|
||||||
|
raise ValidationError('You cannot specify seat category mappings on event creation.')
|
||||||
|
else:
|
||||||
|
return {'seat_category_mapping': {}}
|
||||||
item_cache = {i.pk: i for i in self.instance.items.all()}
|
item_cache = {i.pk: i for i in self.instance.items.all()}
|
||||||
result = {}
|
result = {}
|
||||||
for k, item in value['seat_category_mapping'].items():
|
for k, item in value['seat_category_mapping'].items():
|
||||||
@@ -165,6 +192,7 @@ class EventSerializer(I18nAwareModelSerializer):
|
|||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
meta_data = validated_data.pop('meta_data', None)
|
meta_data = validated_data.pop('meta_data', None)
|
||||||
|
item_meta_properties = validated_data.pop('item_meta_properties', None)
|
||||||
validated_data.pop('seat_category_mapping', None)
|
validated_data.pop('seat_category_mapping', None)
|
||||||
plugins = validated_data.pop('plugins', settings.PRETIX_PLUGINS_DEFAULT.split(','))
|
plugins = validated_data.pop('plugins', settings.PRETIX_PLUGINS_DEFAULT.split(','))
|
||||||
tz = validated_data.pop('timezone', None)
|
tz = validated_data.pop('timezone', None)
|
||||||
@@ -181,6 +209,15 @@ class EventSerializer(I18nAwareModelSerializer):
|
|||||||
value=value
|
value=value
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Item Meta properties
|
||||||
|
if item_meta_properties is not None:
|
||||||
|
for key, value in item_meta_properties.items():
|
||||||
|
event.item_meta_properties.create(
|
||||||
|
name=key,
|
||||||
|
default=value,
|
||||||
|
event=event
|
||||||
|
)
|
||||||
|
|
||||||
# Seats
|
# Seats
|
||||||
if event.seating_plan:
|
if event.seating_plan:
|
||||||
generate_seats(event, None, event.seating_plan, {})
|
generate_seats(event, None, event.seating_plan, {})
|
||||||
@@ -195,6 +232,7 @@ class EventSerializer(I18nAwareModelSerializer):
|
|||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
meta_data = validated_data.pop('meta_data', None)
|
meta_data = validated_data.pop('meta_data', None)
|
||||||
|
item_meta_properties = validated_data.pop('item_meta_properties', None)
|
||||||
plugins = validated_data.pop('plugins', None)
|
plugins = validated_data.pop('plugins', None)
|
||||||
seat_category_mapping = validated_data.pop('seat_category_mapping', None)
|
seat_category_mapping = validated_data.pop('seat_category_mapping', None)
|
||||||
tz = validated_data.pop('timezone', None)
|
tz = validated_data.pop('timezone', None)
|
||||||
@@ -221,6 +259,26 @@ class EventSerializer(I18nAwareModelSerializer):
|
|||||||
if prop.name not in meta_data:
|
if prop.name not in meta_data:
|
||||||
current_object.delete()
|
current_object.delete()
|
||||||
|
|
||||||
|
# Item Meta properties
|
||||||
|
if item_meta_properties is not None:
|
||||||
|
current = [imp for imp in event.item_meta_properties.all()]
|
||||||
|
for key, value in item_meta_properties.items():
|
||||||
|
prop = self.item_meta_props.get(key)
|
||||||
|
if prop in current:
|
||||||
|
prop.default = value
|
||||||
|
prop.save()
|
||||||
|
else:
|
||||||
|
prop = event.item_meta_properties.create(
|
||||||
|
name=key,
|
||||||
|
default=value,
|
||||||
|
event=event
|
||||||
|
)
|
||||||
|
current.append(prop)
|
||||||
|
|
||||||
|
for prop in current:
|
||||||
|
if prop.name not in list(item_meta_properties.keys()):
|
||||||
|
prop.delete()
|
||||||
|
|
||||||
# Seats
|
# Seats
|
||||||
if seat_category_mapping is not None or ('seating_plan' in validated_data and validated_data['seating_plan'] is None):
|
if seat_category_mapping is not None or ('seating_plan' in validated_data and validated_data['seating_plan'] is None):
|
||||||
current_mappings = {
|
current_mappings = {
|
||||||
@@ -466,3 +524,138 @@ class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = TaxRule
|
model = TaxRule
|
||||||
fields = ('id', 'name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country')
|
fields = ('id', 'name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country')
|
||||||
|
|
||||||
|
|
||||||
|
class EventSettingsSerializer(serializers.Serializer):
|
||||||
|
default_fields = [
|
||||||
|
'imprint_url',
|
||||||
|
'checkout_email_helptext',
|
||||||
|
'presale_has_ended_text',
|
||||||
|
'voucher_explanation_text',
|
||||||
|
'banner_text',
|
||||||
|
'banner_text_bottom',
|
||||||
|
'show_dates_on_frontpage',
|
||||||
|
'show_date_to',
|
||||||
|
'show_times',
|
||||||
|
'show_items_outside_presale_period',
|
||||||
|
'display_net_prices',
|
||||||
|
'presale_start_show_date',
|
||||||
|
'locales',
|
||||||
|
'locale',
|
||||||
|
'last_order_modification_date',
|
||||||
|
'show_quota_left',
|
||||||
|
'waiting_list_enabled',
|
||||||
|
'waiting_list_hours',
|
||||||
|
'waiting_list_auto',
|
||||||
|
'max_items_per_order',
|
||||||
|
'reservation_time',
|
||||||
|
'contact_mail',
|
||||||
|
'show_variations_expanded',
|
||||||
|
'hide_sold_out',
|
||||||
|
'meta_noindex',
|
||||||
|
'redirect_to_checkout_directly',
|
||||||
|
'frontpage_subevent_ordering',
|
||||||
|
'frontpage_text',
|
||||||
|
'attendee_names_asked',
|
||||||
|
'attendee_names_required',
|
||||||
|
'attendee_emails_asked',
|
||||||
|
'attendee_emails_required',
|
||||||
|
'attendee_addresses_asked',
|
||||||
|
'attendee_addresses_required',
|
||||||
|
'attendee_company_asked',
|
||||||
|
'attendee_company_required',
|
||||||
|
'confirm_text',
|
||||||
|
'order_email_asked_twice',
|
||||||
|
'payment_term_days',
|
||||||
|
'payment_term_last',
|
||||||
|
'payment_term_weekdays',
|
||||||
|
'payment_term_expire_automatically',
|
||||||
|
'payment_term_accept_late',
|
||||||
|
'payment_explanation',
|
||||||
|
'ticket_download',
|
||||||
|
'ticket_download_date',
|
||||||
|
'ticket_download_addons',
|
||||||
|
'ticket_download_nonadm',
|
||||||
|
'ticket_download_pending',
|
||||||
|
'mail_prefix',
|
||||||
|
'mail_from',
|
||||||
|
'mail_from_name',
|
||||||
|
'mail_attach_ical',
|
||||||
|
'invoice_address_asked',
|
||||||
|
'invoice_address_required',
|
||||||
|
'invoice_address_vatid',
|
||||||
|
'invoice_address_company_required',
|
||||||
|
'invoice_address_beneficiary',
|
||||||
|
'invoice_address_custom_field',
|
||||||
|
'invoice_name_required',
|
||||||
|
'invoice_address_not_asked_free',
|
||||||
|
'invoice_show_payments',
|
||||||
|
'invoice_reissue_after_modify',
|
||||||
|
'invoice_include_free',
|
||||||
|
'invoice_generate',
|
||||||
|
'invoice_numbers_consecutive',
|
||||||
|
'invoice_numbers_prefix',
|
||||||
|
'invoice_numbers_prefix_cancellations',
|
||||||
|
'invoice_attendee_name',
|
||||||
|
'invoice_include_expire_date',
|
||||||
|
'invoice_address_explanation_text',
|
||||||
|
'invoice_email_attachment',
|
||||||
|
'invoice_address_from_name',
|
||||||
|
'invoice_address_from',
|
||||||
|
'invoice_address_from_zipcode',
|
||||||
|
'invoice_address_from_city',
|
||||||
|
'invoice_address_from_country',
|
||||||
|
'invoice_address_from_tax_id',
|
||||||
|
'invoice_address_from_vat_id',
|
||||||
|
'invoice_introductory_text',
|
||||||
|
'invoice_additional_text',
|
||||||
|
'invoice_footer_text',
|
||||||
|
'cancel_allow_user',
|
||||||
|
'cancel_allow_user_until',
|
||||||
|
'cancel_allow_user_paid',
|
||||||
|
'cancel_allow_user_paid_until',
|
||||||
|
'cancel_allow_user_paid_keep',
|
||||||
|
'cancel_allow_user_paid_keep_fees',
|
||||||
|
'cancel_allow_user_paid_keep_percentage',
|
||||||
|
'cancel_allow_user_paid_adjust_fees',
|
||||||
|
'cancel_allow_user_paid_adjust_fees_explanation',
|
||||||
|
'cancel_allow_user_paid_refund_as_giftcard',
|
||||||
|
'cancel_allow_user_paid_require_approval',
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.event = kwargs.pop('event')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
for fname in self.default_fields:
|
||||||
|
kwargs = DEFAULTS[fname].get('serializer_kwargs', {})
|
||||||
|
kwargs.setdefault('required', False)
|
||||||
|
kwargs.setdefault('allow_null', True)
|
||||||
|
form_kwargs = DEFAULTS[fname].get('form_kwargs', {})
|
||||||
|
if 'serializer_class' not in DEFAULTS[fname]:
|
||||||
|
raise ValidationError('{} has no serializer class'.format(fname))
|
||||||
|
f = DEFAULTS[fname]['serializer_class'](
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
f._label = form_kwargs.get('label', fname)
|
||||||
|
f._help_text = form_kwargs.get('help_text')
|
||||||
|
self.fields[fname] = f
|
||||||
|
|
||||||
|
for recv, resp in api_event_settings_fields.send(sender=self.event):
|
||||||
|
for fname, field in resp.items():
|
||||||
|
field.required = False
|
||||||
|
self.fields[fname] = field
|
||||||
|
|
||||||
|
def update(self, instance: HierarkeyProxy, validated_data):
|
||||||
|
for attr, value in validated_data.items():
|
||||||
|
if value is None:
|
||||||
|
instance.delete(attr)
|
||||||
|
elif instance.get(attr, as_type=type(value)) != value:
|
||||||
|
instance.set(attr, value)
|
||||||
|
return instance
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
data = super().validate(data)
|
||||||
|
settings_dict = self.instance.freeze()
|
||||||
|
settings_dict.update(data)
|
||||||
|
validate_settings(self.event, settings_dict)
|
||||||
|
return data
|
||||||
|
|||||||
29
src/pretix/api/serializers/fields.py
Normal file
29
src/pretix/api/serializers/fields.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
|
def remove_duplicates_from_list(data):
|
||||||
|
return list(OrderedDict.fromkeys(data))
|
||||||
|
|
||||||
|
|
||||||
|
class ListMultipleChoiceField(serializers.MultipleChoiceField):
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
if isinstance(data, str) or not hasattr(data, '__iter__'):
|
||||||
|
self.fail('not_a_list', input_type=type(data).__name__)
|
||||||
|
if not self.allow_empty and len(data) == 0:
|
||||||
|
self.fail('empty')
|
||||||
|
|
||||||
|
internal_value_data = [
|
||||||
|
super(serializers.MultipleChoiceField, self).to_internal_value(item)
|
||||||
|
for item in data
|
||||||
|
]
|
||||||
|
|
||||||
|
return remove_duplicates_from_list(internal_value_data)
|
||||||
|
|
||||||
|
def to_representation(self, value):
|
||||||
|
representation_data = [
|
||||||
|
self.choice_strings_to_values.get(str(item), item) for item in value
|
||||||
|
]
|
||||||
|
|
||||||
|
return remove_duplicates_from_list(representation_data)
|
||||||
@@ -2,13 +2,15 @@ from decimal import Decimal
|
|||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.functional import cached_property
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from pretix.api.serializers.event import MetaDataField
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Item, ItemAddOn, ItemBundle, ItemCategory, ItemVariation, Question,
|
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemVariation,
|
||||||
QuestionOption, Quota,
|
Question, QuestionOption, Quota,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -110,6 +112,7 @@ class ItemSerializer(I18nAwareModelSerializer):
|
|||||||
bundles = InlineItemBundleSerializer(many=True, required=False)
|
bundles = InlineItemBundleSerializer(many=True, required=False)
|
||||||
variations = InlineItemVariationSerializer(many=True, required=False)
|
variations = InlineItemVariationSerializer(many=True, required=False)
|
||||||
tax_rate = ItemTaxRateField(source='*', read_only=True)
|
tax_rate = ItemTaxRateField(source='*', read_only=True)
|
||||||
|
meta_data = MetaDataField(required=False, source='*')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Item
|
model = Item
|
||||||
@@ -119,7 +122,7 @@ class ItemSerializer(I18nAwareModelSerializer):
|
|||||||
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
|
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
|
||||||
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations', 'variations',
|
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations', 'variations',
|
||||||
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
|
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
|
||||||
'show_quota_left', 'hidden_if_available', 'allow_waitinglist', 'issue_giftcard')
|
'show_quota_left', 'hidden_if_available', 'allow_waitinglist', 'issue_giftcard', 'meta_data')
|
||||||
read_only_fields = ('has_variations', 'picture')
|
read_only_fields = ('has_variations', 'picture')
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
@@ -167,18 +170,65 @@ class ItemSerializer(I18nAwareModelSerializer):
|
|||||||
ItemAddOn.clean_max_min_count(addon_data['max_count'], addon_data['min_count'])
|
ItemAddOn.clean_max_min_count(addon_data['max_count'], addon_data['min_count'])
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def item_meta_properties(self):
|
||||||
|
return {
|
||||||
|
p.name: p for p in self.context['request'].event.item_meta_properties.all()
|
||||||
|
}
|
||||||
|
|
||||||
|
def validate_meta_data(self, value):
|
||||||
|
for key in value['meta_data'].keys():
|
||||||
|
if key not in self.item_meta_properties:
|
||||||
|
raise ValidationError(_('Item meta data property \'{name}\' does not exist.').format(name=key))
|
||||||
|
return value
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
variations_data = validated_data.pop('variations') if 'variations' in validated_data else {}
|
variations_data = validated_data.pop('variations') if 'variations' in validated_data else {}
|
||||||
addons_data = validated_data.pop('addons') if 'addons' in validated_data else {}
|
addons_data = validated_data.pop('addons') if 'addons' in validated_data else {}
|
||||||
bundles_data = validated_data.pop('bundles') if 'bundles' in validated_data else {}
|
bundles_data = validated_data.pop('bundles') if 'bundles' in validated_data else {}
|
||||||
|
meta_data = validated_data.pop('meta_data', None)
|
||||||
item = Item.objects.create(**validated_data)
|
item = Item.objects.create(**validated_data)
|
||||||
|
|
||||||
for variation_data in variations_data:
|
for variation_data in variations_data:
|
||||||
ItemVariation.objects.create(item=item, **variation_data)
|
ItemVariation.objects.create(item=item, **variation_data)
|
||||||
for addon_data in addons_data:
|
for addon_data in addons_data:
|
||||||
ItemAddOn.objects.create(base_item=item, **addon_data)
|
ItemAddOn.objects.create(base_item=item, **addon_data)
|
||||||
for bundle_data in bundles_data:
|
for bundle_data in bundles_data:
|
||||||
ItemBundle.objects.create(base_item=item, **bundle_data)
|
ItemBundle.objects.create(base_item=item, **bundle_data)
|
||||||
|
|
||||||
|
# Meta data
|
||||||
|
if meta_data is not None:
|
||||||
|
for key, value in meta_data.items():
|
||||||
|
ItemMetaValue.objects.create(
|
||||||
|
property=self.item_meta_properties.get(key),
|
||||||
|
value=value,
|
||||||
|
item=item
|
||||||
|
)
|
||||||
|
return item
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
meta_data = validated_data.pop('meta_data', None)
|
||||||
|
item = super().update(instance, validated_data)
|
||||||
|
|
||||||
|
# Meta data
|
||||||
|
if meta_data is not None:
|
||||||
|
current = {mv.property: mv for mv in item.meta_values.select_related('property')}
|
||||||
|
for key, value in meta_data.items():
|
||||||
|
prop = self.item_meta_properties.get(key)
|
||||||
|
if prop in current:
|
||||||
|
current[prop].value = value
|
||||||
|
current[prop].save()
|
||||||
|
else:
|
||||||
|
item.meta_values.create(
|
||||||
|
property=self.item_meta_properties.get(key),
|
||||||
|
value=value
|
||||||
|
)
|
||||||
|
|
||||||
|
for prop, current_object in current.items():
|
||||||
|
if prop.name not in meta_data:
|
||||||
|
current_object.delete()
|
||||||
|
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
@@ -237,8 +287,8 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
|||||||
if value:
|
if value:
|
||||||
if value.type not in (Question.TYPE_CHOICE, Question.TYPE_BOOLEAN, Question.TYPE_CHOICE_MULTIPLE):
|
if value.type not in (Question.TYPE_CHOICE, Question.TYPE_BOOLEAN, Question.TYPE_CHOICE_MULTIPLE):
|
||||||
raise ValidationError('Question dependencies can only be set to boolean or choice questions.')
|
raise ValidationError('Question dependencies can only be set to boolean or choice questions.')
|
||||||
if value == self.instance:
|
if value == self.instance:
|
||||||
raise ValidationError('A question cannot depend on itself.')
|
raise ValidationError('A question cannot depend on itself.')
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from decimal import Decimal
|
|||||||
import pycountry
|
import pycountry
|
||||||
from django.db.models import F, Q
|
from django.db.models import F, Q
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext_lazy
|
from django.utils.translation import gettext_lazy
|
||||||
from django_countries.fields import Country
|
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.exceptions import ValidationError
|
||||||
@@ -25,6 +25,7 @@ from pretix.base.models.orders import (
|
|||||||
)
|
)
|
||||||
from pretix.base.pdf import get_variables
|
from pretix.base.pdf import get_variables
|
||||||
from pretix.base.services.cart import error_messages
|
from pretix.base.services.cart import error_messages
|
||||||
|
from pretix.base.services.locking import NoLockManager
|
||||||
from pretix.base.services.pricing import get_price
|
from pretix.base.services.pricing import get_price
|
||||||
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
|
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
|
||||||
from pretix.base.signals import register_ticket_outputs
|
from pretix.base.signals import register_ticket_outputs
|
||||||
@@ -38,7 +39,7 @@ class CompatibleCountryField(serializers.Field):
|
|||||||
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)
|
||||||
else:
|
elif hasattr(instance, 'country_old'):
|
||||||
return instance.country_old
|
return instance.country_old
|
||||||
|
|
||||||
|
|
||||||
@@ -96,6 +97,11 @@ class AnswerQuestionOptionsIdentifierField(serializers.Field):
|
|||||||
return [o.identifier for o in instance.options.all()]
|
return [o.identifier for o in instance.options.all()]
|
||||||
|
|
||||||
|
|
||||||
|
class AnswerQuestionOptionsField(serializers.Field):
|
||||||
|
def to_representation(self, instance: QuestionAnswer):
|
||||||
|
return [o.pk for o in instance.options.all()]
|
||||||
|
|
||||||
|
|
||||||
class InlineSeatSerializer(I18nAwareModelSerializer):
|
class InlineSeatSerializer(I18nAwareModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -106,6 +112,7 @@ class InlineSeatSerializer(I18nAwareModelSerializer):
|
|||||||
class AnswerSerializer(I18nAwareModelSerializer):
|
class AnswerSerializer(I18nAwareModelSerializer):
|
||||||
question_identifier = AnswerQuestionIdentifierField(source='*', read_only=True)
|
question_identifier = AnswerQuestionIdentifierField(source='*', read_only=True)
|
||||||
option_identifiers = AnswerQuestionOptionsIdentifierField(source='*', read_only=True)
|
option_identifiers = AnswerQuestionOptionsIdentifierField(source='*', read_only=True)
|
||||||
|
options = AnswerQuestionOptionsField(source='*', read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = QuestionAnswer
|
model = QuestionAnswer
|
||||||
@@ -189,6 +196,11 @@ class PdfDataSerializer(serializers.Field):
|
|||||||
for k, v in ev._cached_meta_data.items():
|
for k, v in ev._cached_meta_data.items():
|
||||||
res['meta:' + k] = v
|
res['meta:' + k] = v
|
||||||
|
|
||||||
|
if not hasattr(instance.item, '_cached_meta_data'):
|
||||||
|
instance.item._cached_meta_data = instance.item.meta_data
|
||||||
|
for k, v in instance.item._cached_meta_data.items():
|
||||||
|
res['itemmeta:' + k] = v
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
@@ -199,10 +211,12 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
|||||||
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
|
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
|
||||||
pdf_data = PdfDataSerializer(source='*')
|
pdf_data = PdfDataSerializer(source='*')
|
||||||
seat = InlineSeatSerializer(read_only=True)
|
seat = InlineSeatSerializer(read_only=True)
|
||||||
|
country = CompatibleCountryField(source='*')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = OrderPosition
|
model = OrderPosition
|
||||||
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
||||||
|
'company', 'street', 'zipcode', 'city', 'country', 'state',
|
||||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled')
|
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled')
|
||||||
|
|
||||||
@@ -504,12 +518,22 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
|||||||
max_digits=10)
|
max_digits=10)
|
||||||
voucher = serializers.SlugRelatedField(slug_field='code', queryset=Voucher.objects.none(),
|
voucher = serializers.SlugRelatedField(slug_field='code', queryset=Voucher.objects.none(),
|
||||||
required=False, allow_null=True)
|
required=False, allow_null=True)
|
||||||
|
country = CompatibleCountryField(source='*')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = OrderPosition
|
model = OrderPosition
|
||||||
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
||||||
|
'company', 'street', 'zipcode', 'city', 'country', 'state',
|
||||||
'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher')
|
'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher')
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
for k, v in self.fields.items():
|
||||||
|
if k in ('company', 'street', 'zipcode', 'city', 'country', 'state'):
|
||||||
|
v.required = False
|
||||||
|
v.allow_blank = True
|
||||||
|
v.allow_null = True
|
||||||
|
|
||||||
def validate_secret(self, secret):
|
def validate_secret(self, secret):
|
||||||
if secret and OrderPosition.all.filter(order__event=self.context['event'], secret=secret).exists():
|
if secret and OrderPosition.all.filter(order__event=self.context['event'], secret=secret).exists():
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
@@ -564,6 +588,24 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
|||||||
)
|
)
|
||||||
if data.get('attendee_name_parts') and '_scheme' not in data.get('attendee_name_parts'):
|
if data.get('attendee_name_parts') and '_scheme' not in data.get('attendee_name_parts'):
|
||||||
data['attendee_name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
|
data['attendee_name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
|
||||||
|
|
||||||
|
if data.get('country'):
|
||||||
|
if not pycountry.countries.get(alpha_2=data.get('country')):
|
||||||
|
raise ValidationError(
|
||||||
|
{'country': ['Invalid country code.']}
|
||||||
|
)
|
||||||
|
|
||||||
|
if data.get('state'):
|
||||||
|
cc = str(data.get('country') or self.instance.country or '')
|
||||||
|
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||||
|
raise ValidationError(
|
||||||
|
{'state': ['States are not supported in country "{}".'.format(cc)]}
|
||||||
|
)
|
||||||
|
if not pycountry.subdivisions.get(code=cc + '-' + data.get('state')):
|
||||||
|
raise ValidationError(
|
||||||
|
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
|
||||||
|
)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@@ -580,6 +622,28 @@ class CompatibleJSONField(serializers.JSONField):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class WrappedList:
|
||||||
|
def __init__(self, data):
|
||||||
|
self._data = data
|
||||||
|
|
||||||
|
def all(self):
|
||||||
|
return self._data
|
||||||
|
|
||||||
|
|
||||||
|
class WrappedModel:
|
||||||
|
def __init__(self, model):
|
||||||
|
self._wrapped = model
|
||||||
|
|
||||||
|
def __getattr__(self, item):
|
||||||
|
return getattr(self._wrapped, item)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def delete(self, *args, **kwargs):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class OrderCreateSerializer(I18nAwareModelSerializer):
|
class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||||
invoice_address = InvoiceAddressSerializer(required=False)
|
invoice_address = InvoiceAddressSerializer(required=False)
|
||||||
positions = OrderPositionCreateSerializer(many=True, required=True)
|
positions = OrderPositionCreateSerializer(many=True, required=True)
|
||||||
@@ -600,6 +664,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
force = serializers.BooleanField(default=False, required=False)
|
force = serializers.BooleanField(default=False, required=False)
|
||||||
payment_date = serializers.DateTimeField(required=False, allow_null=True)
|
payment_date = serializers.DateTimeField(required=False, allow_null=True)
|
||||||
send_mail = serializers.BooleanField(default=False, required=False)
|
send_mail = serializers.BooleanField(default=False, required=False)
|
||||||
|
simulate = serializers.BooleanField(default=False, required=False)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -609,7 +674,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
model = Order
|
model = Order
|
||||||
fields = ('code', 'status', 'testmode', 'email', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
|
fields = ('code', 'status', 'testmode', 'email', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
|
||||||
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts',
|
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts',
|
||||||
'force', 'send_mail')
|
'force', 'send_mail', 'simulate')
|
||||||
|
|
||||||
def validate_payment_provider(self, pp):
|
def validate_payment_provider(self, pp):
|
||||||
if pp is None:
|
if pp is None:
|
||||||
@@ -701,6 +766,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
payment_info = validated_data.pop('payment_info', '{}')
|
payment_info = validated_data.pop('payment_info', '{}')
|
||||||
payment_date = validated_data.pop('payment_date', now())
|
payment_date = validated_data.pop('payment_date', now())
|
||||||
force = validated_data.pop('force', False)
|
force = validated_data.pop('force', False)
|
||||||
|
simulate = validated_data.pop('simulate', False)
|
||||||
self._send_mail = validated_data.pop('send_mail', False)
|
self._send_mail = validated_data.pop('send_mail', False)
|
||||||
|
|
||||||
if 'invoice_address' in validated_data:
|
if 'invoice_address' in validated_data:
|
||||||
@@ -714,7 +780,10 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
else:
|
else:
|
||||||
ia = None
|
ia = None
|
||||||
|
|
||||||
with self.context['event'].lock() as now_dt:
|
lockfn = self.context['event'].lock
|
||||||
|
if simulate:
|
||||||
|
lockfn = NoLockManager
|
||||||
|
with lockfn() as now_dt:
|
||||||
free_seats = set()
|
free_seats = set()
|
||||||
seats_seen = set()
|
seats_seen = set()
|
||||||
consume_carts = validated_data.pop('consume_carts', [])
|
consume_carts = validated_data.pop('consume_carts', [])
|
||||||
@@ -823,7 +892,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
else:
|
else:
|
||||||
pos_data['seat'] = seat
|
pos_data['seat'] = seat
|
||||||
if (seat not in free_seats and not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web'))) or seat in seats_seen:
|
if (seat not in free_seats and not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web'))) or seat in seats_seen:
|
||||||
errs[i]['seat'] = [ugettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)]
|
errs[i]['seat'] = [gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)]
|
||||||
seats_seen.add(seat)
|
seats_seen.add(seat)
|
||||||
elif seated:
|
elif seated:
|
||||||
errs[i]['seat'] = ['The specified product requires to choose a seat.']
|
errs[i]['seat'] = ['The specified product requires to choose a seat.']
|
||||||
@@ -838,7 +907,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
if pos_data.get('variation')
|
if pos_data.get('variation')
|
||||||
else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent')))
|
else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent')))
|
||||||
if len(new_quotas) == 0:
|
if len(new_quotas) == 0:
|
||||||
errs[i]['item'] = [ugettext_lazy('The product "{}" is not assigned to a quota.').format(
|
errs[i]['item'] = [gettext_lazy('The product "{}" is not assigned to a quota.').format(
|
||||||
str(pos_data.get('item'))
|
str(pos_data.get('item'))
|
||||||
)]
|
)]
|
||||||
else:
|
else:
|
||||||
@@ -850,7 +919,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
quota_avail_cache[quota][1] -= 1
|
quota_avail_cache[quota][1] -= 1
|
||||||
if quota_avail_cache[quota][1] < 0:
|
if quota_avail_cache[quota][1] < 0:
|
||||||
errs[i]['item'] = [
|
errs[i]['item'] = [
|
||||||
ugettext_lazy('There is not enough quota available on quota "{}" to perform the operation.').format(
|
gettext_lazy('There is not enough quota available on quota "{}" to perform the operation.').format(
|
||||||
quota.name
|
quota.name
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
@@ -864,11 +933,20 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
|
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
|
||||||
order.meta_info = "{}"
|
order.meta_info = "{}"
|
||||||
order.total = Decimal('0.00')
|
order.total = Decimal('0.00')
|
||||||
order.save()
|
if simulate:
|
||||||
|
order = WrappedModel(order)
|
||||||
|
order.last_modified = now()
|
||||||
|
order.code = 'PREVIEW'
|
||||||
|
else:
|
||||||
|
order.save()
|
||||||
|
|
||||||
if ia:
|
if ia:
|
||||||
ia.order = order
|
if not simulate:
|
||||||
ia.save()
|
ia.order = order
|
||||||
|
ia.save()
|
||||||
|
else:
|
||||||
|
order.invoice_address = ia
|
||||||
|
ia.last_modified = now()
|
||||||
|
|
||||||
pos_map = {}
|
pos_map = {}
|
||||||
for pos_data in positions_data:
|
for pos_data in positions_data:
|
||||||
@@ -880,7 +958,10 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
'_legacy': attendee_name
|
'_legacy': attendee_name
|
||||||
}
|
}
|
||||||
pos = OrderPosition(**pos_data)
|
pos = OrderPosition(**pos_data)
|
||||||
pos.order = order
|
if simulate:
|
||||||
|
pos.order = order._wrapped
|
||||||
|
else:
|
||||||
|
pos.order = order
|
||||||
if addon_to:
|
if addon_to:
|
||||||
pos.addon_to = pos_map[addon_to]
|
pos.addon_to = pos_map[addon_to]
|
||||||
|
|
||||||
@@ -911,19 +992,33 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
invoice_address=ia,
|
invoice_address=ia,
|
||||||
).gross
|
).gross
|
||||||
|
|
||||||
if pos.voucher:
|
if simulate:
|
||||||
Voucher.objects.filter(pk=pos.voucher.pk).update(redeemed=F('redeemed') + 1)
|
pos = WrappedModel(pos)
|
||||||
pos.save()
|
pos.id = 0
|
||||||
|
answers = []
|
||||||
|
for answ_data in answers_data:
|
||||||
|
options = answ_data.pop('options', [])
|
||||||
|
answ = WrappedModel(QuestionAnswer(**answ_data))
|
||||||
|
answ.options = WrappedList(options)
|
||||||
|
answers.append(answ)
|
||||||
|
pos.answers = answers
|
||||||
|
pos.pseudonymization_id = "PREVIEW"
|
||||||
|
else:
|
||||||
|
if pos.voucher:
|
||||||
|
Voucher.objects.filter(pk=pos.voucher.pk).update(redeemed=F('redeemed') + 1)
|
||||||
|
pos.save()
|
||||||
|
for answ_data in answers_data:
|
||||||
|
options = answ_data.pop('options', [])
|
||||||
|
answ = pos.answers.create(**answ_data)
|
||||||
|
answ.options.add(*options)
|
||||||
pos_map[pos.positionid] = pos
|
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:
|
if not simulate:
|
||||||
cp.delete()
|
for cp in delete_cps:
|
||||||
|
cp.delete()
|
||||||
|
|
||||||
order.total = sum([p.price for p in order.positions.all()])
|
order.total = sum([p.price for p in pos_map.values()])
|
||||||
|
fees = []
|
||||||
for fee_data in fees_data:
|
for fee_data in fees_data:
|
||||||
is_percentage = fee_data.pop('_treat_value_as_percentage', False)
|
is_percentage = fee_data.pop('_treat_value_as_percentage', False)
|
||||||
if is_percentage:
|
if is_percentage:
|
||||||
@@ -955,17 +1050,26 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
fee_data['tax_rule'] = tr
|
fee_data['tax_rule'] = tr
|
||||||
fee_data['value'] = val
|
fee_data['value'] = val
|
||||||
f = OrderFee(**fee_data)
|
f = OrderFee(**fee_data)
|
||||||
f.order = order
|
f.order = order._wrapped if simulate else order
|
||||||
f._calculate_tax()
|
f._calculate_tax()
|
||||||
f.save()
|
fees.append(f)
|
||||||
|
if not simulate:
|
||||||
|
f.save()
|
||||||
else:
|
else:
|
||||||
f = OrderFee(**fee_data)
|
f = OrderFee(**fee_data)
|
||||||
f.order = order
|
f.order = order._wrapped if simulate else order
|
||||||
f._calculate_tax()
|
f._calculate_tax()
|
||||||
f.save()
|
fees.append(f)
|
||||||
|
if not simulate:
|
||||||
|
f.save()
|
||||||
|
|
||||||
order.total += sum([f.value for f in order.fees.all()])
|
order.total += sum([f.value for f in fees])
|
||||||
order.save(update_fields=['total'])
|
if simulate:
|
||||||
|
order.fees = fees
|
||||||
|
order.positions = pos_map.values()
|
||||||
|
return order # ignore payments
|
||||||
|
else:
|
||||||
|
order.save(update_fields=['total'])
|
||||||
|
|
||||||
if order.total == Decimal('0.00') and validated_data.get('status') == Order.STATUS_PAID and not payment_provider:
|
if order.total == Decimal('0.00') and validated_data.get('status') == Order.STATUS_PAID and not payment_provider:
|
||||||
payment_provider = 'free'
|
payment_provider = 'free'
|
||||||
@@ -1034,6 +1138,20 @@ class InvoiceSerializer(I18nAwareModelSerializer):
|
|||||||
'internal_reference')
|
'internal_reference')
|
||||||
|
|
||||||
|
|
||||||
|
class OrderPaymentCreateSerializer(I18nAwareModelSerializer):
|
||||||
|
provider = serializers.CharField(required=True, allow_null=False, allow_blank=False)
|
||||||
|
info = CompatibleJSONField(required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = OrderPayment
|
||||||
|
fields = ('state', 'amount', 'payment_date', 'provider', 'info')
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
order = OrderPayment(order=self.context['order'], **validated_data)
|
||||||
|
order.save()
|
||||||
|
return order
|
||||||
|
|
||||||
|
|
||||||
class OrderRefundCreateSerializer(I18nAwareModelSerializer):
|
class OrderRefundCreateSerializer(I18nAwareModelSerializer):
|
||||||
payment = serializers.IntegerField(required=False, allow_null=True)
|
payment = serializers.IntegerField(required=False, allow_null=True)
|
||||||
provider = serializers.CharField(required=True, allow_null=False, allow_blank=False)
|
provider = serializers.CharField(required=True, allow_null=False, allow_blank=False)
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import get_language, gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.api.serializers.order import CompatibleJSONField
|
from pretix.api.serializers.order import CompatibleJSONField
|
||||||
from pretix.base.models import GiftCard, Organizer, SeatingPlan
|
from pretix.base.auth import get_auth_backends
|
||||||
|
from pretix.base.models import (
|
||||||
|
GiftCard, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
|
||||||
|
)
|
||||||
from pretix.base.models.seating import SeatingPlanLayoutValidator
|
from pretix.base.models.seating import SeatingPlanLayoutValidator
|
||||||
|
from pretix.base.services.mail import SendMailException, mail
|
||||||
|
from pretix.helpers.urls import build_absolute_uri
|
||||||
|
|
||||||
|
|
||||||
class OrganizerSerializer(I18nAwareModelSerializer):
|
class OrganizerSerializer(I18nAwareModelSerializer):
|
||||||
@@ -36,16 +41,129 @@ class GiftCardSerializer(I18nAwareModelSerializer):
|
|||||||
qs = GiftCard.objects.filter(
|
qs = GiftCard.objects.filter(
|
||||||
secret=s
|
secret=s
|
||||||
).filter(
|
).filter(
|
||||||
Q(issuer=self.context["organizer"]) | Q(issuer__gift_card_collector_acceptance__collector=self.context["organizer"])
|
Q(issuer=self.context["organizer"]) | Q(
|
||||||
|
issuer__gift_card_collector_acceptance__collector=self.context["organizer"])
|
||||||
)
|
)
|
||||||
if self.instance:
|
if self.instance:
|
||||||
qs = qs.exclude(pk=self.instance.pk)
|
qs = qs.exclude(pk=self.instance.pk)
|
||||||
if qs.exists():
|
if qs.exists():
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
{'secret': _('A gift card with the same secret already exists in your or an affiliated organizer account.')}
|
{'secret': _(
|
||||||
|
'A gift card with the same secret already exists in your or an affiliated organizer account.')}
|
||||||
)
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = GiftCard
|
model = GiftCard
|
||||||
fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode')
|
fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode')
|
||||||
|
|
||||||
|
|
||||||
|
class EventSlugField(serializers.SlugRelatedField):
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.context['organizer'].events.all()
|
||||||
|
|
||||||
|
|
||||||
|
class TeamSerializer(serializers.ModelSerializer):
|
||||||
|
limit_events = EventSlugField(slug_field='slug', many=True)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Team
|
||||||
|
fields = (
|
||||||
|
'id', 'name', 'all_events', 'limit_events', 'can_create_events', 'can_change_teams',
|
||||||
|
'can_change_organizer_settings', 'can_manage_gift_cards', 'can_change_event_settings',
|
||||||
|
'can_change_items', 'can_view_orders', 'can_change_orders', 'can_view_vouchers',
|
||||||
|
'can_change_vouchers'
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
||||||
|
full_data.update(data)
|
||||||
|
if full_data.get('limit_events') and full_data.get('all_events'):
|
||||||
|
raise ValidationError('Do not set both limit_events and all_events.')
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class TeamInviteSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = TeamInvite
|
||||||
|
fields = (
|
||||||
|
'id', 'email'
|
||||||
|
)
|
||||||
|
|
||||||
|
def _send_invite(self, instance):
|
||||||
|
try:
|
||||||
|
mail(
|
||||||
|
instance.email,
|
||||||
|
_('pretix account invitation'),
|
||||||
|
'pretixcontrol/email/invitation.txt',
|
||||||
|
{
|
||||||
|
'user': self,
|
||||||
|
'organizer': self.context['organizer'].name,
|
||||||
|
'team': instance.team.name,
|
||||||
|
'url': build_absolute_uri('control:auth.invite', kwargs={
|
||||||
|
'token': instance.token
|
||||||
|
})
|
||||||
|
},
|
||||||
|
event=None,
|
||||||
|
locale=get_language() # TODO: expose?
|
||||||
|
)
|
||||||
|
except SendMailException:
|
||||||
|
pass # Already logged
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
if 'email' in validated_data:
|
||||||
|
try:
|
||||||
|
user = User.objects.get(email__iexact=validated_data['email'])
|
||||||
|
except User.DoesNotExist:
|
||||||
|
if self.context['team'].invites.filter(email__iexact=validated_data['email']).exists():
|
||||||
|
raise ValidationError(_('This user already has been invited for this team.'))
|
||||||
|
if 'native' not in get_auth_backends():
|
||||||
|
raise ValidationError('Users need to have a pretix account before they can be invited.')
|
||||||
|
|
||||||
|
invite = self.context['team'].invites.create(email=validated_data['email'])
|
||||||
|
self._send_invite(invite)
|
||||||
|
invite.team.log_action(
|
||||||
|
'pretix.team.invite.created',
|
||||||
|
data={
|
||||||
|
'email': validated_data['email']
|
||||||
|
},
|
||||||
|
**self.context['log_kwargs']
|
||||||
|
)
|
||||||
|
return invite
|
||||||
|
else:
|
||||||
|
if self.context['team'].members.filter(pk=user.pk).exists():
|
||||||
|
raise ValidationError(_('This user already has permissions for this team.'))
|
||||||
|
|
||||||
|
self.context['team'].members.add(user)
|
||||||
|
self.context['team'].log_action(
|
||||||
|
'pretix.team.member.added',
|
||||||
|
data={
|
||||||
|
'email': user.email,
|
||||||
|
'user': user.pk,
|
||||||
|
},
|
||||||
|
**self.context['log_kwargs']
|
||||||
|
)
|
||||||
|
return TeamInvite(email=user.email)
|
||||||
|
else:
|
||||||
|
raise ValidationError('No email address given.')
|
||||||
|
|
||||||
|
|
||||||
|
class TeamAPITokenSerializer(serializers.ModelSerializer):
|
||||||
|
active = serializers.BooleanField(default=True, read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = TeamAPIToken
|
||||||
|
fields = (
|
||||||
|
'id', 'name', 'active'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TeamMemberSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = (
|
||||||
|
'id', 'email', 'fullname', 'require_2fa'
|
||||||
|
)
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ from rest_framework import routers
|
|||||||
from pretix.api.views import cart
|
from pretix.api.views import cart
|
||||||
|
|
||||||
from .views import (
|
from .views import (
|
||||||
checkin, device, event, item, oauth, order, organizer, user, voucher,
|
checkin, device, event, item, oauth, order, organizer, user, version,
|
||||||
waitinglist, webhooks,
|
voucher, waitinglist, webhooks,
|
||||||
)
|
)
|
||||||
|
|
||||||
router = routers.DefaultRouter()
|
router = routers.DefaultRouter()
|
||||||
@@ -20,6 +20,12 @@ orga_router.register(r'subevents', event.SubEventViewSet)
|
|||||||
orga_router.register(r'webhooks', webhooks.WebHookViewSet)
|
orga_router.register(r'webhooks', webhooks.WebHookViewSet)
|
||||||
orga_router.register(r'seatingplans', organizer.SeatingPlanViewSet)
|
orga_router.register(r'seatingplans', organizer.SeatingPlanViewSet)
|
||||||
orga_router.register(r'giftcards', organizer.GiftCardViewSet)
|
orga_router.register(r'giftcards', organizer.GiftCardViewSet)
|
||||||
|
orga_router.register(r'teams', organizer.TeamViewSet)
|
||||||
|
|
||||||
|
team_router = routers.DefaultRouter()
|
||||||
|
team_router.register(r'members', organizer.TeamMemberViewSet)
|
||||||
|
team_router.register(r'invites', organizer.TeamInviteViewSet)
|
||||||
|
team_router.register(r'tokens', organizer.TeamAPITokenViewSet)
|
||||||
|
|
||||||
event_router = routers.DefaultRouter()
|
event_router = routers.DefaultRouter()
|
||||||
event_router.register(r'subevents', event.SubEventViewSet)
|
event_router.register(r'subevents', event.SubEventViewSet)
|
||||||
@@ -61,7 +67,10 @@ for app in apps.get_app_configs():
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^', include(router.urls)),
|
url(r'^', include(router.urls)),
|
||||||
url(r'^organizers/(?P<organizer>[^/]+)/', include(orga_router.urls)),
|
url(r'^organizers/(?P<organizer>[^/]+)/', include(orga_router.urls)),
|
||||||
|
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/settings/$', event.EventSettingsView.as_view(),
|
||||||
|
name="event.settings"),
|
||||||
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/', include(event_router.urls)),
|
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/', include(event_router.urls)),
|
||||||
|
url(r'^organizers/(?P<organizer>[^/]+)/teams/(?P<team>[^/]+)/', include(team_router.urls)),
|
||||||
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/items/(?P<item>[^/]+)/', include(item_router.urls)),
|
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/items/(?P<item>[^/]+)/', include(item_router.urls)),
|
||||||
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/questions/(?P<question>[^/]+)/',
|
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/questions/(?P<question>[^/]+)/',
|
||||||
include(question_router.urls)),
|
include(question_router.urls)),
|
||||||
@@ -76,4 +85,5 @@ urlpatterns = [
|
|||||||
url(r"^device/roll$", device.RollKeyView.as_view(), name="device.roll"),
|
url(r"^device/roll$", device.RollKeyView.as_view(), name="device.roll"),
|
||||||
url(r"^device/revoke$", device.RevokeKeyView.as_view(), name="device.revoke"),
|
url(r"^device/revoke$", device.RevokeKeyView.as_view(), name="device.revoke"),
|
||||||
url(r"^me$", user.MeView.as_view(), name="user.me"),
|
url(r"^me$", user.MeView.as_view(), name="user.me"),
|
||||||
|
url(r"^version$", version.VersionView.as_view(), name="version"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ from django.db.models import ProtectedError, Q
|
|||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
from rest_framework import filters, viewsets
|
from rest_framework import filters, views, viewsets
|
||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from pretix.api.auth.permission import EventCRUDPermission
|
from pretix.api.auth.permission import EventCRUDPermission
|
||||||
from pretix.api.serializers.event import (
|
from pretix.api.serializers.event import (
|
||||||
CloneEventSerializer, EventSerializer, SubEventSerializer,
|
CloneEventSerializer, EventSerializer, EventSettingsSerializer,
|
||||||
TaxRuleSerializer,
|
SubEventSerializer, TaxRuleSerializer,
|
||||||
)
|
)
|
||||||
from pretix.api.views import ConditionalListView
|
from pretix.api.views import ConditionalListView
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
@@ -333,3 +334,33 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
auth=self.request.auth,
|
auth=self.request.auth,
|
||||||
)
|
)
|
||||||
super().perform_destroy(instance)
|
super().perform_destroy(instance)
|
||||||
|
|
||||||
|
|
||||||
|
class EventSettingsView(views.APIView):
|
||||||
|
permission = 'can_change_event_settings'
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
s = EventSettingsSerializer(instance=request.event.settings, event=request.event)
|
||||||
|
if 'explain' in request.GET:
|
||||||
|
return Response({
|
||||||
|
fname: {
|
||||||
|
'value': s.data[fname],
|
||||||
|
'label': getattr(field, '_label', fname),
|
||||||
|
'help_text': getattr(field, '_help_text', None)
|
||||||
|
} for fname, field in s.fields.items()
|
||||||
|
})
|
||||||
|
return Response(s.data)
|
||||||
|
|
||||||
|
def patch(self, request, *wargs, **kwargs):
|
||||||
|
s = EventSettingsSerializer(instance=request.event.settings, data=request.data, partial=True,
|
||||||
|
event=request.event)
|
||||||
|
s.is_valid(raise_exception=True)
|
||||||
|
with transaction.atomic():
|
||||||
|
s.save()
|
||||||
|
self.request.event.log_action(
|
||||||
|
'pretix.event.settings', user=self.request.user, auth=self.request.auth, data={
|
||||||
|
k: v for k, v in s.validated_data.items()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
s = EventSettingsSerializer(instance=request.event.settings, event=request.event)
|
||||||
|
return Response(s.data)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import logging
|
|||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
from oauth2_provider.exceptions import OAuthToolkitError
|
from oauth2_provider.exceptions import OAuthToolkitError
|
||||||
from oauth2_provider.forms import AllowForm
|
from oauth2_provider.forms import AllowForm
|
||||||
from oauth2_provider.views import (
|
from oauth2_provider.views import (
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from django.db.models.functions import Coalesce, Concat
|
|||||||
from django.http import FileResponse, HttpResponse
|
from django.http import FileResponse, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.timezone import make_aware, now
|
from django.utils.timezone import make_aware, now
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
from rest_framework import mixins, serializers, status, viewsets
|
from rest_framework import mixins, serializers, status, viewsets
|
||||||
@@ -23,9 +23,10 @@ from rest_framework.response import Response
|
|||||||
|
|
||||||
from pretix.api.models import OAuthAccessToken
|
from pretix.api.models import OAuthAccessToken
|
||||||
from pretix.api.serializers.order import (
|
from pretix.api.serializers.order import (
|
||||||
InvoiceSerializer, OrderCreateSerializer, OrderPaymentSerializer,
|
InvoiceSerializer, OrderCreateSerializer, OrderPaymentCreateSerializer,
|
||||||
OrderPositionSerializer, OrderRefundCreateSerializer,
|
OrderPaymentSerializer, OrderPositionSerializer,
|
||||||
OrderRefundSerializer, OrderSerializer, PriceCalcSerializer,
|
OrderRefundCreateSerializer, OrderRefundSerializer, OrderSerializer,
|
||||||
|
PriceCalcSerializer,
|
||||||
)
|
)
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
@@ -43,7 +44,7 @@ from pretix.base.services.mail import SendMailException
|
|||||||
from pretix.base.services.orders import (
|
from pretix.base.services.orders import (
|
||||||
OrderChangeManager, OrderError, _order_placed_email,
|
OrderChangeManager, OrderError, _order_placed_email,
|
||||||
_order_placed_email_attendee, approve_order, cancel_order, deny_order,
|
_order_placed_email_attendee, approve_order, cancel_order, deny_order,
|
||||||
extend_order, mark_order_expired, mark_order_refunded,
|
extend_order, mark_order_expired, mark_order_refunded, reactivate_order,
|
||||||
)
|
)
|
||||||
from pretix.base.services.pricing import get_price
|
from pretix.base.services.pricing import get_price
|
||||||
from pretix.base.services.tickets import generate
|
from pretix.base.services.tickets import generate
|
||||||
@@ -260,6 +261,29 @@ class OrderViewSet(viewsets.ModelViewSet):
|
|||||||
)
|
)
|
||||||
return self.retrieve(request, [], **kwargs)
|
return self.retrieve(request, [], **kwargs)
|
||||||
|
|
||||||
|
@action(detail=True, methods=['POST'])
|
||||||
|
def reactivate(self, request, **kwargs):
|
||||||
|
|
||||||
|
order = self.get_object()
|
||||||
|
if order.status != Order.STATUS_CANCELED:
|
||||||
|
return Response(
|
||||||
|
{'detail': 'The order is not allowed to be reactivated.'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
reactivate_order(
|
||||||
|
order,
|
||||||
|
user=request.user if request.user.is_authenticated else None,
|
||||||
|
auth=request.auth if isinstance(request.auth, (Device, TeamAPIToken, OAuthAccessToken)) else None,
|
||||||
|
)
|
||||||
|
except OrderError as e:
|
||||||
|
return Response(
|
||||||
|
{'detail': str(e)},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
return self.retrieve(request, [], **kwargs)
|
||||||
|
|
||||||
@action(detail=True, methods=['POST'])
|
@action(detail=True, methods=['POST'])
|
||||||
def approve(self, request, **kwargs):
|
def approve(self, request, **kwargs):
|
||||||
send_mail = request.data.get('send_email', True)
|
send_mail = request.data.get('send_email', True)
|
||||||
@@ -465,6 +489,9 @@ class OrderViewSet(viewsets.ModelViewSet):
|
|||||||
send_mail = serializer._send_mail
|
send_mail = serializer._send_mail
|
||||||
order = serializer.instance
|
order = serializer.instance
|
||||||
serializer = OrderSerializer(order, context=serializer.context)
|
serializer = OrderSerializer(order, context=serializer.context)
|
||||||
|
if not order.pk:
|
||||||
|
# Simulation
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
order.log_action(
|
order.log_action(
|
||||||
'pretix.event.order.placed',
|
'pretix.event.order.placed',
|
||||||
@@ -825,17 +852,62 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
|
|||||||
raise ValidationError(str(e))
|
raise ValidationError(str(e))
|
||||||
|
|
||||||
|
|
||||||
class PaymentViewSet(viewsets.ReadOnlyModelViewSet):
|
class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||||
serializer_class = OrderPaymentSerializer
|
serializer_class = OrderPaymentSerializer
|
||||||
queryset = OrderPayment.objects.none()
|
queryset = OrderPayment.objects.none()
|
||||||
permission = 'can_view_orders'
|
permission = 'can_view_orders'
|
||||||
write_permission = 'can_change_orders'
|
write_permission = 'can_change_orders'
|
||||||
lookup_field = 'local_id'
|
lookup_field = 'local_id'
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
ctx = super().get_serializer_context()
|
||||||
|
ctx['order'] = get_object_or_404(Order, code=self.kwargs['order'], event=self.request.event)
|
||||||
|
return ctx
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
order = get_object_or_404(Order, code=self.kwargs['order'], event=self.request.event)
|
order = get_object_or_404(Order, code=self.kwargs['order'], event=self.request.event)
|
||||||
return order.payments.all()
|
return order.payments.all()
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
serializer = OrderPaymentCreateSerializer(data=request.data, context=self.get_serializer_context())
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
with transaction.atomic():
|
||||||
|
mark_confirmed = False
|
||||||
|
if serializer.validated_data['state'] == OrderPayment.PAYMENT_STATE_CONFIRMED:
|
||||||
|
serializer.validated_data['state'] = OrderPayment.PAYMENT_STATE_PENDING
|
||||||
|
mark_confirmed = True
|
||||||
|
self.perform_create(serializer)
|
||||||
|
r = serializer.instance
|
||||||
|
if mark_confirmed:
|
||||||
|
try:
|
||||||
|
r.confirm(
|
||||||
|
user=self.request.user if self.request.user.is_authenticated else None,
|
||||||
|
auth=self.request.auth,
|
||||||
|
count_waitinglist=False,
|
||||||
|
force=request.data.get('force', False)
|
||||||
|
)
|
||||||
|
except Quota.QuotaExceededException:
|
||||||
|
pass
|
||||||
|
except SendMailException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
serializer = OrderPaymentSerializer(r, context=serializer.context)
|
||||||
|
|
||||||
|
r.order.log_action(
|
||||||
|
'pretix.event.order.payment.started', {
|
||||||
|
'local_id': r.local_id,
|
||||||
|
'provider': r.provider,
|
||||||
|
},
|
||||||
|
user=request.user if request.user.is_authenticated else None,
|
||||||
|
auth=request.auth
|
||||||
|
)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
@action(detail=True, methods=['POST'])
|
@action(detail=True, methods=['POST'])
|
||||||
def confirm(self, request, **kwargs):
|
def confirm(self, request, **kwargs):
|
||||||
payment = self.get_object()
|
payment = self.get_object()
|
||||||
@@ -1015,6 +1087,7 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
|||||||
mark_refunded = request.data.pop('mark_refunded', False)
|
mark_refunded = request.data.pop('mark_refunded', False)
|
||||||
else:
|
else:
|
||||||
mark_refunded = request.data.pop('mark_canceled', False)
|
mark_refunded = request.data.pop('mark_canceled', False)
|
||||||
|
mark_pending = request.data.pop('mark_pending', False)
|
||||||
serializer = OrderRefundCreateSerializer(data=request.data, context=self.get_serializer_context())
|
serializer = OrderRefundCreateSerializer(data=request.data, context=self.get_serializer_context())
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
@@ -1031,11 +1104,23 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
|||||||
auth=request.auth
|
auth=request.auth
|
||||||
)
|
)
|
||||||
if mark_refunded:
|
if mark_refunded:
|
||||||
mark_order_refunded(
|
try:
|
||||||
r.order,
|
mark_order_refunded(
|
||||||
user=request.user if request.user.is_authenticated else None,
|
r.order,
|
||||||
auth=(request.auth if request.auth else None),
|
user=request.user if request.user.is_authenticated else None,
|
||||||
)
|
auth=(request.auth if request.auth else None),
|
||||||
|
)
|
||||||
|
except OrderError as e:
|
||||||
|
raise ValidationError(str(e))
|
||||||
|
elif mark_pending:
|
||||||
|
if r.order.status == Order.STATUS_PAID and r.order.pending_sum > 0:
|
||||||
|
r.order.status = Order.STATUS_PENDING
|
||||||
|
r.order.set_expires(
|
||||||
|
now(),
|
||||||
|
r.order.event.subevents.filter(
|
||||||
|
id__in=r.order.positions.values_list('subevent_id', flat=True))
|
||||||
|
)
|
||||||
|
r.order.save(update_fields=['status', 'expires'])
|
||||||
|
|
||||||
headers = self.get_success_headers(serializer.data)
|
headers = self.get_success_headers(serializer.data)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||||
|
|||||||
@@ -1,16 +1,26 @@
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import django_filters
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
from rest_framework import filters, serializers, status, viewsets
|
from rest_framework import filters, serializers, status, viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import MethodNotAllowed, PermissionDenied
|
from rest_framework.exceptions import MethodNotAllowed, PermissionDenied
|
||||||
|
from rest_framework.mixins import CreateModelMixin, DestroyModelMixin
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from pretix.api.models import OAuthAccessToken
|
from pretix.api.models import OAuthAccessToken
|
||||||
from pretix.api.serializers.organizer import (
|
from pretix.api.serializers.organizer import (
|
||||||
GiftCardSerializer, OrganizerSerializer, SeatingPlanSerializer,
|
GiftCardSerializer, OrganizerSerializer, SeatingPlanSerializer,
|
||||||
|
TeamAPITokenSerializer, TeamInviteSerializer, TeamMemberSerializer,
|
||||||
|
TeamSerializer,
|
||||||
|
)
|
||||||
|
from pretix.base.models import (
|
||||||
|
GiftCard, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
|
||||||
)
|
)
|
||||||
from pretix.base.models import GiftCard, Organizer, SeatingPlan
|
|
||||||
from pretix.helpers.dicts import merge_dicts
|
from pretix.helpers.dicts import merge_dicts
|
||||||
|
|
||||||
|
|
||||||
@@ -48,13 +58,14 @@ class SeatingPlanViewSet(viewsets.ModelViewSet):
|
|||||||
write_permission = 'can_change_organizer_settings'
|
write_permission = 'can_change_organizer_settings'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.organizer.seating_plans.all()
|
return self.request.organizer.seating_plans.order_by('name')
|
||||||
|
|
||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
ctx = super().get_serializer_context()
|
ctx = super().get_serializer_context()
|
||||||
ctx['organizer'] = self.request.organizer
|
ctx['organizer'] = self.request.organizer
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
@transaction.atomic()
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
inst = serializer.save(organizer=self.request.organizer)
|
inst = serializer.save(organizer=self.request.organizer)
|
||||||
self.request.organizer.log_action(
|
self.request.organizer.log_action(
|
||||||
@@ -64,6 +75,7 @@ class SeatingPlanViewSet(viewsets.ModelViewSet):
|
|||||||
data=merge_dicts(self.request.data, {'id': inst.pk})
|
data=merge_dicts(self.request.data, {'id': inst.pk})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@transaction.atomic()
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
if serializer.instance.events.exists() or serializer.instance.subevents.exists():
|
if serializer.instance.events.exists() or serializer.instance.subevents.exists():
|
||||||
raise PermissionDenied('This plan can not be changed while it is in use for an event.')
|
raise PermissionDenied('This plan can not be changed while it is in use for an event.')
|
||||||
@@ -76,6 +88,7 @@ class SeatingPlanViewSet(viewsets.ModelViewSet):
|
|||||||
)
|
)
|
||||||
return inst
|
return inst
|
||||||
|
|
||||||
|
@transaction.atomic()
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
if instance.events.exists() or instance.subevents.exists():
|
if instance.events.exists() or instance.subevents.exists():
|
||||||
raise PermissionDenied('This plan can not be deleted while it is in use for an event.')
|
raise PermissionDenied('This plan can not be deleted while it is in use for an event.')
|
||||||
@@ -88,14 +101,29 @@ class SeatingPlanViewSet(viewsets.ModelViewSet):
|
|||||||
instance.delete()
|
instance.delete()
|
||||||
|
|
||||||
|
|
||||||
|
with scopes_disabled():
|
||||||
|
class GiftCardFilter(FilterSet):
|
||||||
|
secret = django_filters.CharFilter(field_name='secret', lookup_expr='iexact')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = GiftCard
|
||||||
|
fields = ['secret', 'testmode']
|
||||||
|
|
||||||
|
|
||||||
class GiftCardViewSet(viewsets.ModelViewSet):
|
class GiftCardViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = GiftCardSerializer
|
serializer_class = GiftCardSerializer
|
||||||
queryset = GiftCard.objects.none()
|
queryset = GiftCard.objects.none()
|
||||||
permission = 'can_manage_gift_cards'
|
permission = 'can_manage_gift_cards'
|
||||||
write_permission = 'can_manage_gift_cards'
|
write_permission = 'can_manage_gift_cards'
|
||||||
|
filter_backends = (DjangoFilterBackend,)
|
||||||
|
filterset_class = GiftCardFilter
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.organizer.issued_gift_cards.all()
|
if self.request.GET.get('include_accepted') == 'true':
|
||||||
|
qs = self.request.organizer.accepted_gift_cards
|
||||||
|
else:
|
||||||
|
qs = self.request.organizer.issued_gift_cards.all()
|
||||||
|
return qs
|
||||||
|
|
||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
ctx = super().get_serializer_context()
|
ctx = super().get_serializer_context()
|
||||||
@@ -116,6 +144,8 @@ class GiftCardViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
@transaction.atomic()
|
@transaction.atomic()
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
|
if 'include_accepted' in self.request.GET:
|
||||||
|
raise PermissionDenied("Accepted gift cards cannot be updated, use transact instead.")
|
||||||
GiftCard.objects.select_for_update().get(pk=self.get_object().pk)
|
GiftCard.objects.select_for_update().get(pk=self.get_object().pk)
|
||||||
old_value = serializer.instance.value
|
old_value = serializer.instance.value
|
||||||
value = serializer.validated_data.pop('value')
|
value = serializer.validated_data.pop('value')
|
||||||
@@ -138,18 +168,187 @@ class GiftCardViewSet(viewsets.ModelViewSet):
|
|||||||
value = serializers.DecimalField(max_digits=10, decimal_places=2).to_internal_value(
|
value = serializers.DecimalField(max_digits=10, decimal_places=2).to_internal_value(
|
||||||
request.data.get('value')
|
request.data.get('value')
|
||||||
)
|
)
|
||||||
|
text = serializers.CharField(allow_blank=True, allow_null=True).to_internal_value(
|
||||||
|
request.data.get('text', '')
|
||||||
|
)
|
||||||
if gc.value + value < Decimal('0.00'):
|
if gc.value + value < Decimal('0.00'):
|
||||||
return Response({
|
return Response({
|
||||||
'value': ['The gift card does not have sufficient credit for this operation.']
|
'value': ['The gift card does not have sufficient credit for this operation.']
|
||||||
}, status=status.HTTP_409_CONFLICT)
|
}, status=status.HTTP_409_CONFLICT)
|
||||||
gc.transactions.create(value=value)
|
gc.transactions.create(value=value, text=text)
|
||||||
gc.log_action(
|
gc.log_action(
|
||||||
'pretix.giftcards.transaction.manual',
|
'pretix.giftcards.transaction.manual',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
auth=self.request.auth,
|
auth=self.request.auth,
|
||||||
data={'value': value}
|
data={'value': value, 'text': text}
|
||||||
)
|
)
|
||||||
return Response(GiftCardSerializer(gc).data, status=status.HTTP_200_OK)
|
return Response(GiftCardSerializer(gc).data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
raise MethodNotAllowed("Gift cards cannot be deleted.")
|
raise MethodNotAllowed("Gift cards cannot be deleted.")
|
||||||
|
|
||||||
|
|
||||||
|
class TeamViewSet(viewsets.ModelViewSet):
|
||||||
|
serializer_class = TeamSerializer
|
||||||
|
queryset = Team.objects.none()
|
||||||
|
permission = 'can_change_teams'
|
||||||
|
write_permission = 'can_change_teams'
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.request.organizer.teams.order_by('pk')
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
ctx = super().get_serializer_context()
|
||||||
|
ctx['organizer'] = self.request.organizer
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
@transaction.atomic()
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
inst = serializer.save(organizer=self.request.organizer)
|
||||||
|
inst.log_action(
|
||||||
|
'pretix.team.created',
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
data=merge_dicts(self.request.data, {'id': inst.pk})
|
||||||
|
)
|
||||||
|
|
||||||
|
@transaction.atomic()
|
||||||
|
def perform_update(self, serializer):
|
||||||
|
inst = serializer.save()
|
||||||
|
inst.log_action(
|
||||||
|
'pretix.team.changed',
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
data=self.request.data
|
||||||
|
)
|
||||||
|
return inst
|
||||||
|
|
||||||
|
def perform_destroy(self, instance):
|
||||||
|
instance.log_action('pretix.team.deleted', user=self.request.user, auth=self.request.auth)
|
||||||
|
instance.delete()
|
||||||
|
|
||||||
|
|
||||||
|
class TeamMemberViewSet(DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||||
|
serializer_class = TeamMemberSerializer
|
||||||
|
queryset = User.objects.none()
|
||||||
|
permission = 'can_change_teams'
|
||||||
|
write_permission = 'can_change_teams'
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def team(self):
|
||||||
|
return get_object_or_404(self.request.organizer.teams, pk=self.kwargs.get('team'))
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.team.members.all()
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
ctx = super().get_serializer_context()
|
||||||
|
ctx['organizer'] = self.request.organizer
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
@transaction.atomic()
|
||||||
|
def perform_destroy(self, instance):
|
||||||
|
self.team.members.remove(instance)
|
||||||
|
self.team.log_action(
|
||||||
|
'pretix.team.member.removed', user=self.request.user, auth=self.request.auth, data={
|
||||||
|
'email': instance.email,
|
||||||
|
'user': instance.pk
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TeamInviteViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||||
|
serializer_class = TeamInviteSerializer
|
||||||
|
queryset = TeamInvite.objects.none()
|
||||||
|
permission = 'can_change_teams'
|
||||||
|
write_permission = 'can_change_teams'
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def team(self):
|
||||||
|
return get_object_or_404(self.request.organizer.teams, pk=self.kwargs.get('team'))
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.team.invites.order_by('email')
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
ctx = super().get_serializer_context()
|
||||||
|
ctx['organizer'] = self.request.organizer
|
||||||
|
ctx['team'] = self.team
|
||||||
|
ctx['log_kwargs'] = {
|
||||||
|
'user': self.request.user,
|
||||||
|
'auth': self.request.auth,
|
||||||
|
}
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
@transaction.atomic()
|
||||||
|
def perform_destroy(self, instance):
|
||||||
|
self.team.log_action(
|
||||||
|
'pretix.team.invite.deleted', user=self.request.user, auth=self.request.auth, data={
|
||||||
|
'email': instance.email,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
instance.delete()
|
||||||
|
|
||||||
|
@transaction.atomic()
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
serializer.save(team=self.team)
|
||||||
|
|
||||||
|
|
||||||
|
class TeamAPITokenViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||||
|
serializer_class = TeamAPITokenSerializer
|
||||||
|
queryset = TeamAPIToken.objects.none()
|
||||||
|
permission = 'can_change_teams'
|
||||||
|
write_permission = 'can_change_teams'
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def team(self):
|
||||||
|
return get_object_or_404(self.request.organizer.teams, pk=self.kwargs.get('team'))
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.team.tokens.order_by('name')
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
ctx = super().get_serializer_context()
|
||||||
|
ctx['organizer'] = self.request.organizer
|
||||||
|
ctx['team'] = self.team
|
||||||
|
ctx['log_kwargs'] = {
|
||||||
|
'user': self.request.user,
|
||||||
|
'auth': self.request.auth,
|
||||||
|
}
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
@transaction.atomic()
|
||||||
|
def perform_destroy(self, instance):
|
||||||
|
instance.active = False
|
||||||
|
instance.save()
|
||||||
|
self.team.log_action(
|
||||||
|
'pretix.team.token.deleted', user=self.request.user, auth=self.request.auth, data={
|
||||||
|
'name': instance.name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@transaction.atomic()
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
instance = serializer.save(team=self.team)
|
||||||
|
self.team.log_action(
|
||||||
|
'pretix.team.token.created', auth=self.request.auth, user=self.request.user, data={
|
||||||
|
'name': instance.name,
|
||||||
|
'id': instance.pk
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
serializer = self.get_serializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
self.perform_create(serializer)
|
||||||
|
headers = self.get_success_headers(serializer.data)
|
||||||
|
d = serializer.data
|
||||||
|
d['token'] = serializer.instance.token
|
||||||
|
return Response(d, status=status.HTTP_201_CREATED, headers=headers)
|
||||||
|
|
||||||
|
def destroy(self, request, *args, **kwargs):
|
||||||
|
instance = self.get_object()
|
||||||
|
self.perform_destroy(instance)
|
||||||
|
serializer = self.get_serializer_class()(instance)
|
||||||
|
headers = self.get_success_headers(serializer.data)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK, headers=headers)
|
||||||
|
|||||||
56
src/pretix/api/views/version.py
Normal file
56
src/pretix/api/views/version.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
from oauth2_provider.contrib.rest_framework import OAuth2Authentication
|
||||||
|
from packaging import version
|
||||||
|
from rest_framework.authentication import SessionAuthentication
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
from pretix import __version__
|
||||||
|
from pretix.api.auth.device import DeviceTokenAuthentication
|
||||||
|
from pretix.api.auth.token import TeamTokenAuthentication
|
||||||
|
|
||||||
|
|
||||||
|
def numeric_version(v):
|
||||||
|
# Converts a pretix version to a large int
|
||||||
|
# e.g. 30060001000
|
||||||
|
# |--------------------- Major version
|
||||||
|
# |-|------------------ Minor version
|
||||||
|
# |-|--------------- Patch version
|
||||||
|
# ||------------- Stage (10 dev, 20 alpha, 30 beta, 40 rc, 50 release, 60 post)
|
||||||
|
# ||----------- Stage version (number of dev/alpha/beta/rc/post release)
|
||||||
|
v = version.parse(v)
|
||||||
|
phases = {
|
||||||
|
'dev': 10,
|
||||||
|
'a': 20,
|
||||||
|
'b': 30,
|
||||||
|
'rc': 40,
|
||||||
|
'release': 50,
|
||||||
|
'post': 60
|
||||||
|
}
|
||||||
|
vnum = 0
|
||||||
|
|
||||||
|
if v.is_postrelease:
|
||||||
|
vnum += v.post
|
||||||
|
vnum += phases['post'] * 100
|
||||||
|
elif v.dev is not None:
|
||||||
|
vnum += v.dev
|
||||||
|
vnum += phases['dev'] * 100
|
||||||
|
elif v.is_prerelease and v.pre:
|
||||||
|
vnum += v.pre[0]
|
||||||
|
vnum += phases[v.pre[1]] * 100
|
||||||
|
else:
|
||||||
|
vnum += phases['release'] * 100
|
||||||
|
for i, part in enumerate(reversed(v.release)):
|
||||||
|
vnum += part * (1000 ** i) * 10000
|
||||||
|
return vnum
|
||||||
|
|
||||||
|
|
||||||
|
class VersionView(APIView):
|
||||||
|
authentication_classes = (
|
||||||
|
SessionAuthentication, OAuth2Authentication, DeviceTokenAuthentication, TeamTokenAuthentication
|
||||||
|
)
|
||||||
|
|
||||||
|
def get(self, request, format=None):
|
||||||
|
return Response({
|
||||||
|
'pretix': __version__,
|
||||||
|
'pretix_numeric': numeric_version(__version__),
|
||||||
|
})
|
||||||
@@ -7,7 +7,7 @@ import requests
|
|||||||
from celery.exceptions import MaxRetriesExceededError
|
from celery.exceptions import MaxRetriesExceededError
|
||||||
from django.db.models import Exists, OuterRef, Q
|
from django.db.models import Exists, OuterRef, Q
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_scopes import scope, scopes_disabled
|
from django_scopes import scope, scopes_disabled
|
||||||
from requests import RequestException
|
from requests import RequestException
|
||||||
|
|
||||||
@@ -125,6 +125,10 @@ def register_default_webhook_events(sender, **kwargs):
|
|||||||
'pretix.event.order.canceled',
|
'pretix.event.order.canceled',
|
||||||
_('Order canceled'),
|
_('Order canceled'),
|
||||||
),
|
),
|
||||||
|
ParametrizedOrderWebhookEvent(
|
||||||
|
'pretix.event.order.reactivated',
|
||||||
|
_('Order reactivated'),
|
||||||
|
),
|
||||||
ParametrizedOrderWebhookEvent(
|
ParametrizedOrderWebhookEvent(
|
||||||
'pretix.event.order.expired',
|
'pretix.event.order.expired',
|
||||||
_('Order expired'),
|
_('Order expired'),
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
|
|
||||||
class PretixBaseConfig(AppConfig):
|
class PretixBaseConfig(AppConfig):
|
||||||
@@ -14,6 +13,7 @@ class PretixBaseConfig(AppConfig):
|
|||||||
from . import notifications # NOQA
|
from . import notifications # NOQA
|
||||||
from . import email # NOQA
|
from . import email # NOQA
|
||||||
from .services import auth, checkin, export, mail, tickets, cart, orderimport, orders, invoices, cleanup, update_check, quotas, notifications, vouchers # NOQA
|
from .services import auth, checkin, export, mail, tickets, cart, orderimport, orders, invoices, cleanup, update_check, quotas, notifications, vouchers # NOQA
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from .celery_app import app as celery_app # NOQA
|
from .celery_app import app as celery_app # NOQA
|
||||||
|
|||||||
@@ -85,6 +85,16 @@ class BaseAuthBackend:
|
|||||||
"""
|
"""
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def get_next_url(self, request):
|
||||||
|
"""
|
||||||
|
This method will be called after a successful login to determine the next URL. Pretix in general uses the
|
||||||
|
``'next'`` query parameter. However, external authentication methods could use custom attributes with hardcoded
|
||||||
|
names for security purposes. For example, OAuth uses ``'state'`` for keeping track of application state.
|
||||||
|
"""
|
||||||
|
if "next" in request.GET:
|
||||||
|
return request.GET.get("next")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class NativeAuthBackend(BaseAuthBackend):
|
class NativeAuthBackend(BaseAuthBackend):
|
||||||
identifier = 'native'
|
identifier = 'native'
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import logging
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.signals import register_sales_channels
|
from pretix.base.signals import register_sales_channels
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from django.core.mail.backends.smtp import EmailBackend
|
|||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import get_language, ugettext_lazy as _
|
from django.utils.translation import get_language, gettext_lazy as _
|
||||||
from inlinestyler.utils import inline_css
|
from inlinestyler.utils import inline_css
|
||||||
|
|
||||||
from pretix.base.i18n import LazyCurrencyNumber, LazyDate, LazyNumber
|
from pretix.base.i18n import LazyCurrencyNumber, LazyDate, LazyNumber
|
||||||
@@ -136,15 +136,22 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
|||||||
|
|
||||||
|
|
||||||
class ClassicMailRenderer(TemplateBasedMailRenderer):
|
class ClassicMailRenderer(TemplateBasedMailRenderer):
|
||||||
verbose_name = _('pretix default')
|
verbose_name = _('Default')
|
||||||
identifier = 'classic'
|
identifier = 'classic'
|
||||||
thumbnail_filename = 'pretixbase/email/thumb.png'
|
thumbnail_filename = 'pretixbase/email/thumb.png'
|
||||||
template_name = 'pretixbase/email/plainwrapper.html'
|
template_name = 'pretixbase/email/plainwrapper.html'
|
||||||
|
|
||||||
|
|
||||||
|
class UnembellishedMailRenderer(TemplateBasedMailRenderer):
|
||||||
|
verbose_name = _('Simple with logo')
|
||||||
|
identifier = 'simple_logo'
|
||||||
|
thumbnail_filename = 'pretixbase/email/thumb_simple_logo.png'
|
||||||
|
template_name = 'pretixbase/email/simple_logo.html'
|
||||||
|
|
||||||
|
|
||||||
@receiver(register_html_mail_renderers, dispatch_uid="pretixbase_email_renderers")
|
@receiver(register_html_mail_renderers, dispatch_uid="pretixbase_email_renderers")
|
||||||
def base_renderers(sender, **kwargs):
|
def base_renderers(sender, **kwargs):
|
||||||
return [ClassicMailRenderer]
|
return [ClassicMailRenderer, UnembellishedMailRenderer]
|
||||||
|
|
||||||
|
|
||||||
class BaseMailTextPlaceholder:
|
class BaseMailTextPlaceholder:
|
||||||
@@ -260,6 +267,10 @@ def base_placeholders(sender, **kwargs):
|
|||||||
SimpleFunctionalMailTextPlaceholder(
|
SimpleFunctionalMailTextPlaceholder(
|
||||||
'event', ['event'], lambda event: event.name, lambda event: event.name
|
'event', ['event'], lambda event: event.name, lambda event: event.name
|
||||||
),
|
),
|
||||||
|
SimpleFunctionalMailTextPlaceholder(
|
||||||
|
'event', ['event_or_subevent'], lambda event_or_subevent: event_or_subevent.name,
|
||||||
|
lambda event_or_subevent: event_or_subevent.name
|
||||||
|
),
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
SimpleFunctionalMailTextPlaceholder(
|
||||||
'event_slug', ['event'], lambda event: event.slug, lambda event: event.slug
|
'event_slug', ['event'], lambda event: event.slug, lambda event: event.slug
|
||||||
),
|
),
|
||||||
@@ -272,6 +283,11 @@ def base_placeholders(sender, **kwargs):
|
|||||||
SimpleFunctionalMailTextPlaceholder(
|
SimpleFunctionalMailTextPlaceholder(
|
||||||
'currency', ['event'], lambda event: event.currency, lambda event: event.currency
|
'currency', ['event'], lambda event: event.currency, lambda event: event.currency
|
||||||
),
|
),
|
||||||
|
SimpleFunctionalMailTextPlaceholder(
|
||||||
|
'refund_amount', ['event_or_subevent', 'refund_amount'],
|
||||||
|
lambda event_or_subevent, refund_amount: LazyCurrencyNumber(refund_amount, event_or_subevent.currency),
|
||||||
|
lambda event_or_subevent: LazyCurrencyNumber(Decimal('42.23'), event_or_subevent.currency)
|
||||||
|
),
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
SimpleFunctionalMailTextPlaceholder(
|
||||||
'total_with_currency', ['event', 'order'], lambda event, order: LazyCurrencyNumber(order.total,
|
'total_with_currency', ['event', 'order'], lambda event, order: LazyCurrencyNumber(order.total,
|
||||||
event.currency),
|
event.currency),
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import io
|
import io
|
||||||
import tempfile
|
import tempfile
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
from decimal import Decimal
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
from defusedcsv import csv
|
from defusedcsv import csv
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import ugettext, ugettext_lazy as _
|
from django.utils.formats import localize
|
||||||
|
from django.utils.translation import gettext, gettext_lazy as _
|
||||||
from openpyxl import Workbook
|
from openpyxl import Workbook
|
||||||
from openpyxl.cell.cell import KNOWN_TYPES
|
from openpyxl.cell.cell import KNOWN_TYPES
|
||||||
|
|
||||||
@@ -117,12 +119,20 @@ class ListExporter(BaseExporter):
|
|||||||
if output_file:
|
if output_file:
|
||||||
writer = csv.writer(output_file, **kwargs)
|
writer = csv.writer(output_file, **kwargs)
|
||||||
for line in self.iterate_list(form_data):
|
for line in self.iterate_list(form_data):
|
||||||
|
line = [
|
||||||
|
localize(f) if isinstance(f, Decimal) else f
|
||||||
|
for f in line
|
||||||
|
]
|
||||||
writer.writerow(line)
|
writer.writerow(line)
|
||||||
return self.get_filename() + '.csv', 'text/csv', None
|
return self.get_filename() + '.csv', 'text/csv', None
|
||||||
else:
|
else:
|
||||||
output = io.StringIO()
|
output = io.StringIO()
|
||||||
writer = csv.writer(output, **kwargs)
|
writer = csv.writer(output, **kwargs)
|
||||||
for line in self.iterate_list(form_data):
|
for line in self.iterate_list(form_data):
|
||||||
|
line = [
|
||||||
|
localize(f) if isinstance(f, Decimal) else f
|
||||||
|
for f in line
|
||||||
|
]
|
||||||
writer.writerow(line)
|
writer.writerow(line)
|
||||||
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
|
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
|
||||||
|
|
||||||
@@ -170,9 +180,9 @@ class MultiSheetListExporter(ListExporter):
|
|||||||
]
|
]
|
||||||
for s, l in self.sheets:
|
for s, l in self.sheets:
|
||||||
choices += [
|
choices += [
|
||||||
(s + ':default', str(l) + ' – ' + ugettext('CSV (with commas)')),
|
(s + ':default', str(l) + ' – ' + gettext('CSV (with commas)')),
|
||||||
(s + ':excel', str(l) + ' – ' + ugettext('CSV (Excel-style)')),
|
(s + ':excel', str(l) + ' – ' + gettext('CSV (Excel-style)')),
|
||||||
(s + ':semicolon', str(l) + ' – ' + ugettext('CSV (with semicolons)')),
|
(s + ':semicolon', str(l) + ' – ' + gettext('CSV (with semicolons)')),
|
||||||
]
|
]
|
||||||
ff = OrderedDict(
|
ff = OrderedDict(
|
||||||
[
|
[
|
||||||
@@ -196,12 +206,20 @@ class MultiSheetListExporter(ListExporter):
|
|||||||
if output_file:
|
if output_file:
|
||||||
writer = csv.writer(output_file, **kwargs)
|
writer = csv.writer(output_file, **kwargs)
|
||||||
for line in self.iterate_sheet(form_data, sheet):
|
for line in self.iterate_sheet(form_data, sheet):
|
||||||
|
line = [
|
||||||
|
localize(f) if isinstance(f, Decimal) else f
|
||||||
|
for f in line
|
||||||
|
]
|
||||||
writer.writerow(line)
|
writer.writerow(line)
|
||||||
return self.get_filename() + '.csv', 'text/csv', None
|
return self.get_filename() + '.csv', 'text/csv', None
|
||||||
else:
|
else:
|
||||||
output = io.StringIO()
|
output = io.StringIO()
|
||||||
writer = csv.writer(output, **kwargs)
|
writer = csv.writer(output, **kwargs)
|
||||||
for line in self.iterate_sheet(form_data, sheet):
|
for line in self.iterate_sheet(form_data, sheet):
|
||||||
|
line = [
|
||||||
|
localize(f) if isinstance(f, Decimal) else f
|
||||||
|
for f in line
|
||||||
|
]
|
||||||
writer.writerow(line)
|
writer.writerow(line)
|
||||||
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
|
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from zipfile import ZipFile
|
|||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.models import QuestionAnswer
|
from pretix.base.models import QuestionAnswer
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import dateutil
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.translation import ugettext, ugettext_lazy
|
from django.utils.translation import gettext, gettext_lazy
|
||||||
|
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.models import Invoice, OrderPayment
|
from pretix.base.models import Invoice, OrderPayment
|
||||||
@@ -79,7 +79,7 @@ class DekodiNREIExporter(BaseExporter):
|
|||||||
payments.append({
|
payments.append({
|
||||||
'PTID': '5',
|
'PTID': '5',
|
||||||
'PTN': 'Lastschrift',
|
'PTN': 'Lastschrift',
|
||||||
'PTNo4': ugettext('Event ticket {event}-{code}').format(
|
'PTNo4': gettext('Event ticket {event}-{code}').format(
|
||||||
event=self.event.slug.upper(),
|
event=self.event.slug.upper(),
|
||||||
code=invoice.order.code
|
code=invoice.order.code
|
||||||
),
|
),
|
||||||
@@ -199,19 +199,19 @@ class DekodiNREIExporter(BaseExporter):
|
|||||||
[
|
[
|
||||||
('date_from',
|
('date_from',
|
||||||
forms.DateField(
|
forms.DateField(
|
||||||
label=ugettext_lazy('Start date'),
|
label=gettext_lazy('Start date'),
|
||||||
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
|
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
|
||||||
required=False,
|
required=False,
|
||||||
help_text=ugettext_lazy('Only include invoices issued on or after this date. Note that the invoice date does '
|
help_text=gettext_lazy('Only include invoices issued on or after this date. Note that the invoice date does '
|
||||||
'not always correspond to the order or payment date.')
|
'not always correspond to the order or payment date.')
|
||||||
)),
|
)),
|
||||||
('date_to',
|
('date_to',
|
||||||
forms.DateField(
|
forms.DateField(
|
||||||
label=ugettext_lazy('End date'),
|
label=gettext_lazy('End date'),
|
||||||
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
|
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
|
||||||
required=False,
|
required=False,
|
||||||
help_text=ugettext_lazy('Only include invoices issued on or before this date. Note that the invoice date '
|
help_text=gettext_lazy('Only include invoices issued on or before this date. Note that the invoice date '
|
||||||
'does not always correspond to the order or payment date.')
|
'does not always correspond to the order or payment date.')
|
||||||
)),
|
)),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import dateutil.parser
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.db.models import Exists, OuterRef, Q
|
from django.db.models import Exists, OuterRef, Q
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.models import OrderPayment
|
from pretix.base.models import OrderPayment
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from collections import OrderedDict
|
|||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.models import OrderPosition
|
from pretix.base.models import OrderPosition
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,15 @@ from decimal import Decimal
|
|||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db.models import DateTimeField, F, Max, OuterRef, Subquery, Sum
|
from django.db.models import (
|
||||||
|
Count, DateTimeField, F, IntegerField, Max, OuterRef, Subquery, Sum,
|
||||||
|
)
|
||||||
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
|
||||||
from django.utils.translation import pgettext, ugettext as _, ugettext_lazy
|
from django.utils.translation import gettext as _, gettext_lazy, pgettext
|
||||||
|
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
InvoiceAddress, InvoiceLine, Order, OrderPosition, Question,
|
GiftCard, InvoiceAddress, InvoiceLine, Order, OrderPosition, Question,
|
||||||
)
|
)
|
||||||
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
|
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
|
||||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||||
@@ -20,7 +22,7 @@ from ..signals import register_data_exporters
|
|||||||
|
|
||||||
class OrderListExporter(MultiSheetListExporter):
|
class OrderListExporter(MultiSheetListExporter):
|
||||||
identifier = 'orderlist'
|
identifier = 'orderlist'
|
||||||
verbose_name = ugettext_lazy('Order data')
|
verbose_name = gettext_lazy('Order data')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sheets(self):
|
def sheets(self):
|
||||||
@@ -80,8 +82,12 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
'm'
|
'm'
|
||||||
).order_by()
|
).order_by()
|
||||||
|
|
||||||
|
s = OrderPosition.objects.filter(
|
||||||
|
order=OuterRef('pk')
|
||||||
|
).order_by().values('order').annotate(k=Count('id')).values('k')
|
||||||
qs = self.event.orders.annotate(
|
qs = self.event.orders.annotate(
|
||||||
payment_date=Subquery(p_date, output_field=DateTimeField())
|
payment_date=Subquery(p_date, output_field=DateTimeField()),
|
||||||
|
pcnt=Subquery(s, output_field=IntegerField())
|
||||||
).select_related('invoice_address').prefetch_related('invoices')
|
).select_related('invoice_address').prefetch_related('invoices')
|
||||||
if form_data['paid_only']:
|
if form_data['paid_only']:
|
||||||
qs = qs.filter(status=Order.STATUS_PAID)
|
qs = qs.filter(status=Order.STATUS_PAID)
|
||||||
@@ -111,6 +117,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
headers.append(_('Sales channel'))
|
headers.append(_('Sales channel'))
|
||||||
headers.append(_('Requires special attention'))
|
headers.append(_('Requires special attention'))
|
||||||
headers.append(_('Comment'))
|
headers.append(_('Comment'))
|
||||||
|
headers.append(_('Positions'))
|
||||||
|
|
||||||
yield headers
|
yield headers
|
||||||
|
|
||||||
@@ -134,7 +141,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
for order in qs.order_by('datetime'):
|
for order in qs.order_by('datetime'):
|
||||||
row = [
|
row = [
|
||||||
order.code,
|
order.code,
|
||||||
localize(order.total),
|
order.total,
|
||||||
order.get_status_display(),
|
order.get_status_display(),
|
||||||
order.email,
|
order.email,
|
||||||
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
|
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
|
||||||
@@ -163,7 +170,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
|
|
||||||
row += [
|
row += [
|
||||||
order.payment_date.astimezone(tz).strftime('%Y-%m-%d') if order.payment_date else '',
|
order.payment_date.astimezone(tz).strftime('%Y-%m-%d') if order.payment_date else '',
|
||||||
localize(full_fee_sum_cache.get(order.id) or Decimal('0.00')),
|
full_fee_sum_cache.get(order.id) or Decimal('0.00'),
|
||||||
order.locale,
|
order.locale,
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -173,16 +180,19 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
{'grosssum': Decimal('0.00'), 'taxsum': Decimal('0.00')})
|
{'grosssum': Decimal('0.00'), 'taxsum': Decimal('0.00')})
|
||||||
|
|
||||||
row += [
|
row += [
|
||||||
localize(taxrate_values['grosssum'] + fee_taxrate_values['grosssum']),
|
taxrate_values['grosssum'] + fee_taxrate_values['grosssum'],
|
||||||
localize(taxrate_values['grosssum'] - taxrate_values['taxsum']
|
(
|
||||||
+ fee_taxrate_values['grosssum'] - fee_taxrate_values['taxsum']),
|
taxrate_values['grosssum'] - taxrate_values['taxsum'] +
|
||||||
localize(taxrate_values['taxsum'] + fee_taxrate_values['taxsum']),
|
fee_taxrate_values['grosssum'] - fee_taxrate_values['taxsum']
|
||||||
|
),
|
||||||
|
taxrate_values['taxsum'] + fee_taxrate_values['taxsum'],
|
||||||
]
|
]
|
||||||
|
|
||||||
row.append(', '.join([i.number for i in order.invoices.all()]))
|
row.append(', '.join([i.number for i in order.invoices.all()]))
|
||||||
row.append(order.sales_channel)
|
row.append(order.sales_channel)
|
||||||
row.append(_('Yes') if order.checkin_attention else _('No'))
|
row.append(_('Yes') if order.checkin_attention else _('No'))
|
||||||
row.append(order.comment or "")
|
row.append(order.comment or "")
|
||||||
|
row.append(order.pcnt)
|
||||||
yield row
|
yield row
|
||||||
|
|
||||||
def iterate_fees(self, form_data: dict):
|
def iterate_fees(self, form_data: dict):
|
||||||
@@ -264,7 +274,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
'order', 'order__invoice_address', 'item', 'variation',
|
'order', 'order__invoice_address', 'item', 'variation',
|
||||||
'voucher', 'tax_rule'
|
'voucher', 'tax_rule'
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'answers', 'answers__question'
|
'answers', 'answers__question', 'answers__options'
|
||||||
)
|
)
|
||||||
if form_data['paid_only']:
|
if form_data['paid_only']:
|
||||||
qs = qs.filter(order__status=Order.STATUS_PAID)
|
qs = qs.filter(order__status=Order.STATUS_PAID)
|
||||||
@@ -295,12 +305,25 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
headers.append(_('Attendee name') + ': ' + str(label))
|
headers.append(_('Attendee name') + ': ' + str(label))
|
||||||
headers += [
|
headers += [
|
||||||
_('Attendee email'),
|
_('Attendee email'),
|
||||||
|
_('Company'),
|
||||||
|
_('Address'),
|
||||||
|
_('ZIP code'),
|
||||||
|
_('City'),
|
||||||
|
_('Country'),
|
||||||
|
pgettext('address', 'State'),
|
||||||
_('Voucher'),
|
_('Voucher'),
|
||||||
_('Pseudonymization ID'),
|
_('Pseudonymization ID'),
|
||||||
]
|
]
|
||||||
questions = list(self.event.questions.all())
|
questions = list(self.event.questions.all())
|
||||||
|
options = {}
|
||||||
for q in questions:
|
for q in questions:
|
||||||
headers.append(str(q.question))
|
if q.type == Question.TYPE_CHOICE_MULTIPLE:
|
||||||
|
options[q.pk] = []
|
||||||
|
for o in q.options.all():
|
||||||
|
headers.append(str(q.question) + ' – ' + str(o.answer))
|
||||||
|
options[q.pk].append(o)
|
||||||
|
else:
|
||||||
|
headers.append(str(q.question))
|
||||||
headers += [
|
headers += [
|
||||||
_('Company'),
|
_('Company'),
|
||||||
_('Invoice address name'),
|
_('Invoice address name'),
|
||||||
@@ -347,6 +370,12 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
)
|
)
|
||||||
row += [
|
row += [
|
||||||
op.attendee_email,
|
op.attendee_email,
|
||||||
|
op.company or '',
|
||||||
|
op.street or '',
|
||||||
|
op.zipcode or '',
|
||||||
|
op.city or '',
|
||||||
|
op.country if op.country else '',
|
||||||
|
op.state or '',
|
||||||
op.voucher.code if op.voucher else '',
|
op.voucher.code if op.voucher else '',
|
||||||
op.pseudonymization_id,
|
op.pseudonymization_id,
|
||||||
]
|
]
|
||||||
@@ -354,12 +383,19 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
for a in op.answers.all():
|
for a in op.answers.all():
|
||||||
# We do not want to localize Date, Time and Datetime question answers, as those can lead
|
# We do not want to localize Date, Time and Datetime question answers, as those can lead
|
||||||
# to difficulties parsing the data (for example 2019-02-01 may become Février, 2019 01 in French).
|
# to difficulties parsing the data (for example 2019-02-01 may become Février, 2019 01 in French).
|
||||||
if a.question.type in Question.UNLOCALIZED_TYPES:
|
if a.question.type == Question.TYPE_CHOICE_MULTIPLE:
|
||||||
|
acache[a.question_id] = set(o.pk for o in a.options.all())
|
||||||
|
elif a.question.type in Question.UNLOCALIZED_TYPES:
|
||||||
acache[a.question_id] = a.answer
|
acache[a.question_id] = a.answer
|
||||||
else:
|
else:
|
||||||
acache[a.question_id] = str(a)
|
acache[a.question_id] = str(a)
|
||||||
for q in questions:
|
for q in questions:
|
||||||
row.append(acache.get(q.pk, ''))
|
if q.type == Question.TYPE_CHOICE_MULTIPLE:
|
||||||
|
for o in options[q.pk]:
|
||||||
|
row.append(_('Yes') if o.pk in acache.get(q.pk, set()) else _('No'))
|
||||||
|
else:
|
||||||
|
row.append(acache.get(q.pk, ''))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
row += [
|
row += [
|
||||||
order.invoice_address.company,
|
order.invoice_address.company,
|
||||||
@@ -390,7 +426,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
|
|
||||||
class PaymentListExporter(ListExporter):
|
class PaymentListExporter(ListExporter):
|
||||||
identifier = 'paymentlist'
|
identifier = 'paymentlist'
|
||||||
verbose_name = ugettext_lazy('Order payments and refunds')
|
verbose_name = gettext_lazy('Order payments and refunds')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def additional_form_fields(self):
|
def additional_form_fields(self):
|
||||||
@@ -450,7 +486,7 @@ class PaymentListExporter(ListExporter):
|
|||||||
d2,
|
d2,
|
||||||
obj.get_state_display(),
|
obj.get_state_display(),
|
||||||
obj.state,
|
obj.state,
|
||||||
localize(obj.amount * (-1 if isinstance(obj, OrderRefund) else 1)),
|
obj.amount * (-1 if isinstance(obj, OrderRefund) else 1),
|
||||||
provider_names.get(obj.provider, obj.provider)
|
provider_names.get(obj.provider, obj.provider)
|
||||||
]
|
]
|
||||||
yield row
|
yield row
|
||||||
@@ -461,7 +497,7 @@ class PaymentListExporter(ListExporter):
|
|||||||
|
|
||||||
class QuotaListExporter(ListExporter):
|
class QuotaListExporter(ListExporter):
|
||||||
identifier = 'quotalist'
|
identifier = 'quotalist'
|
||||||
verbose_name = ugettext_lazy('Quota availabilities')
|
verbose_name = gettext_lazy('Quota availabilities')
|
||||||
|
|
||||||
def iterate_list(self, form_data):
|
def iterate_list(self, form_data):
|
||||||
headers = [
|
headers = [
|
||||||
@@ -490,7 +526,7 @@ class QuotaListExporter(ListExporter):
|
|||||||
|
|
||||||
class InvoiceDataExporter(MultiSheetListExporter):
|
class InvoiceDataExporter(MultiSheetListExporter):
|
||||||
identifier = 'invoicedata'
|
identifier = 'invoicedata'
|
||||||
verbose_name = ugettext_lazy('Invoice data')
|
verbose_name = gettext_lazy('Invoice data')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sheets(self):
|
def sheets(self):
|
||||||
@@ -531,10 +567,11 @@ class InvoiceDataExporter(MultiSheetListExporter):
|
|||||||
_('Foreign currency rate'),
|
_('Foreign currency rate'),
|
||||||
_('Total value (with taxes)'),
|
_('Total value (with taxes)'),
|
||||||
_('Total value (without taxes)'),
|
_('Total value (without taxes)'),
|
||||||
|
_('Payment matching IDs'),
|
||||||
]
|
]
|
||||||
qs = self.event.invoices.order_by('full_invoice_no').select_related(
|
qs = self.event.invoices.order_by('full_invoice_no').select_related(
|
||||||
'order', 'refers'
|
'order', 'refers'
|
||||||
).annotate(
|
).prefetch_related('order__payments').annotate(
|
||||||
total_gross=Subquery(
|
total_gross=Subquery(
|
||||||
InvoiceLine.objects.filter(
|
InvoiceLine.objects.filter(
|
||||||
invoice=OuterRef('pk')
|
invoice=OuterRef('pk')
|
||||||
@@ -551,6 +588,16 @@ class InvoiceDataExporter(MultiSheetListExporter):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
for i in qs:
|
for i in qs:
|
||||||
|
pmis = []
|
||||||
|
for p in i.order.payments.all():
|
||||||
|
if p.state in (OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_CREATED,
|
||||||
|
OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_REFUNDED):
|
||||||
|
pprov = p.payment_provider
|
||||||
|
if pprov:
|
||||||
|
mid = pprov.matching_id(p)
|
||||||
|
if mid:
|
||||||
|
pmis.append(mid)
|
||||||
|
pmi = '\n'.join(pmis)
|
||||||
yield [
|
yield [
|
||||||
i.full_invoice_no,
|
i.full_invoice_no,
|
||||||
date_format(i.date, "SHORT_DATE_FORMAT"),
|
date_format(i.date, "SHORT_DATE_FORMAT"),
|
||||||
@@ -581,6 +628,7 @@ class InvoiceDataExporter(MultiSheetListExporter):
|
|||||||
i.foreign_currency_rate,
|
i.foreign_currency_rate,
|
||||||
i.total_gross if i.total_gross else Decimal('0.00'),
|
i.total_gross if i.total_gross else Decimal('0.00'),
|
||||||
Decimal(i.total_net if i.total_net else '0.00').quantize(Decimal('0.01')),
|
Decimal(i.total_net if i.total_net else '0.00').quantize(Decimal('0.01')),
|
||||||
|
pmi
|
||||||
]
|
]
|
||||||
elif sheet == 'lines':
|
elif sheet == 'lines':
|
||||||
yield [
|
yield [
|
||||||
@@ -662,6 +710,45 @@ class InvoiceDataExporter(MultiSheetListExporter):
|
|||||||
return '{}_invoices'.format(self.event.slug)
|
return '{}_invoices'.format(self.event.slug)
|
||||||
|
|
||||||
|
|
||||||
|
class GiftcardRedemptionListExporter(ListExporter):
|
||||||
|
identifier = 'giftcardredemptionlist'
|
||||||
|
verbose_name = gettext_lazy('Giftcard Redemptions')
|
||||||
|
|
||||||
|
def iterate_list(self, form_data):
|
||||||
|
tz = pytz.timezone(self.event.settings.timezone)
|
||||||
|
|
||||||
|
payments = OrderPayment.objects.filter(
|
||||||
|
order__event=self.event,
|
||||||
|
provider='giftcard'
|
||||||
|
).order_by('created')
|
||||||
|
refunds = OrderRefund.objects.filter(
|
||||||
|
order__event=self.event,
|
||||||
|
provider='giftcard'
|
||||||
|
).order_by('created')
|
||||||
|
|
||||||
|
objs = sorted(list(payments) + list(refunds), key=lambda o: (o.order.code, o.created))
|
||||||
|
|
||||||
|
headers = [
|
||||||
|
_('Order'), _('Payment ID'), _('Date'), _('Gift card code'), _('Amount'), _('Issuer')
|
||||||
|
]
|
||||||
|
yield headers
|
||||||
|
|
||||||
|
for obj in objs:
|
||||||
|
gc = GiftCard.objects.get(pk=obj.info_data.get('gift_card'))
|
||||||
|
row = [
|
||||||
|
obj.order.code,
|
||||||
|
obj.full_id,
|
||||||
|
obj.created.astimezone(tz).date().strftime('%Y-%m-%d'),
|
||||||
|
gc.secret,
|
||||||
|
obj.amount * (-1 if isinstance(obj, OrderRefund) else 1),
|
||||||
|
gc.issuer
|
||||||
|
]
|
||||||
|
yield row
|
||||||
|
|
||||||
|
def get_filename(self):
|
||||||
|
return '{}_giftcardredemptions'.format(self.event.slug)
|
||||||
|
|
||||||
|
|
||||||
@receiver(register_data_exporters, dispatch_uid="exporter_orderlist")
|
@receiver(register_data_exporters, dispatch_uid="exporter_orderlist")
|
||||||
def register_orderlist_exporter(sender, **kwargs):
|
def register_orderlist_exporter(sender, **kwargs):
|
||||||
return OrderListExporter
|
return OrderListExporter
|
||||||
@@ -680,3 +767,8 @@ def register_quotalist_exporter(sender, **kwargs):
|
|||||||
@receiver(register_data_exporters, dispatch_uid="exporter_invoicedata")
|
@receiver(register_data_exporters, dispatch_uid="exporter_invoicedata")
|
||||||
def register_invoicedata_exporter(sender, **kwargs):
|
def register_invoicedata_exporter(sender, **kwargs):
|
||||||
return InvoiceDataExporter
|
return InvoiceDataExporter
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(register_data_exporters, dispatch_uid="exporter_giftcardredemptionlist")
|
||||||
|
def register_giftcardredemptionlist_exporter(sender, **kwargs):
|
||||||
|
return GiftcardRedemptionListExporter
|
||||||
|
|||||||
@@ -3,12 +3,10 @@ import logging
|
|||||||
import i18nfield.forms
|
import i18nfield.forms
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.forms.models import ModelFormMetaclass
|
from django.forms.models import ModelFormMetaclass
|
||||||
from django.utils import six
|
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from formtools.wizard.views import SessionWizardView
|
from formtools.wizard.views import SessionWizardView
|
||||||
from hierarkey.forms import HierarkeyForm
|
from hierarkey.forms import HierarkeyForm
|
||||||
|
|
||||||
from pretix.base.models import Event
|
|
||||||
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
|
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
|
||||||
|
|
||||||
from .validators import PlaceholderValidator # NOQA
|
from .validators import PlaceholderValidator # NOQA
|
||||||
@@ -26,7 +24,7 @@ class BaseI18nModelForm(i18nfield.forms.BaseI18nModelForm):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class I18nModelForm(six.with_metaclass(ModelFormMetaclass, BaseI18nModelForm)):
|
class I18nModelForm(BaseI18nModelForm, metaclass=ModelFormMetaclass):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -51,19 +49,33 @@ class I18nInlineFormSet(i18nfield.forms.I18nInlineFormSet):
|
|||||||
|
|
||||||
|
|
||||||
class SettingsForm(i18nfield.forms.I18nFormMixin, HierarkeyForm):
|
class SettingsForm(i18nfield.forms.I18nFormMixin, HierarkeyForm):
|
||||||
|
auto_fields = []
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
from pretix.base.settings import DEFAULTS
|
||||||
|
|
||||||
self.obj = kwargs.get('obj', None)
|
self.obj = kwargs.get('obj', None)
|
||||||
self.locales = self.obj.settings.get('locales') if self.obj else kwargs.pop('locales', None)
|
self.locales = self.obj.settings.get('locales') if self.obj else kwargs.pop('locales', None)
|
||||||
kwargs['attribute_name'] = 'settings'
|
kwargs['attribute_name'] = 'settings'
|
||||||
kwargs['locales'] = self.locales
|
kwargs['locales'] = self.locales
|
||||||
kwargs['initial'] = self.obj.settings.freeze()
|
kwargs['initial'] = self.obj.settings.freeze()
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
for fname in self.auto_fields:
|
||||||
|
kwargs = DEFAULTS[fname].get('form_kwargs', {})
|
||||||
|
kwargs.setdefault('required', False)
|
||||||
|
field = DEFAULTS[fname]['form_class'](
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
if isinstance(field, i18nfield.forms.I18nFormField):
|
||||||
|
field.widget.enabled_locales = self.locales
|
||||||
|
self.fields[fname] = field
|
||||||
for k, f in self.fields.items():
|
for k, f in self.fields.items():
|
||||||
if isinstance(f, (RelativeDateTimeField, RelativeDateField)):
|
if isinstance(f, (RelativeDateTimeField, RelativeDateField)):
|
||||||
f.set_event(self.obj)
|
f.set_event(self.obj)
|
||||||
|
|
||||||
def get_new_filename(self, name: str) -> str:
|
def get_new_filename(self, name: str) -> str:
|
||||||
|
from pretix.base.models import Event
|
||||||
|
|
||||||
nonce = get_random_string(length=8)
|
nonce = get_random_string(length=8)
|
||||||
if isinstance(self.obj, Event):
|
if isinstance(self.obj, Event):
|
||||||
fname = '%s/%s/%s.%s.%s' % (
|
fname = '%s/%s/%s.%s.%s' % (
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ from django.conf import settings
|
|||||||
from django.contrib.auth.password_validation import (
|
from django.contrib.auth.password_validation import (
|
||||||
password_validators_help_texts, validate_password,
|
password_validators_help_texts, validate_password,
|
||||||
)
|
)
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.models import User
|
from pretix.base.models import User
|
||||||
|
from pretix.helpers.dicts import move_to_end
|
||||||
|
|
||||||
|
|
||||||
class LoginForm(forms.Form):
|
class LoginForm(forms.Form):
|
||||||
@@ -36,7 +37,7 @@ class LoginForm(forms.Form):
|
|||||||
if not settings.PRETIX_LONG_SESSIONS or backend.url:
|
if not settings.PRETIX_LONG_SESSIONS or backend.url:
|
||||||
del self.fields['keep_logged_in']
|
del self.fields['keep_logged_in']
|
||||||
else:
|
else:
|
||||||
self.fields.move_to_end('keep_logged_in')
|
move_to_end(self.fields, 'keep_logged_in')
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if all(k in self.cleaned_data for k, f in self.fields.items() if f.required):
|
if all(k in self.cleaned_data for k, f in self.fields.items() if f.required):
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from django.forms import Select
|
|||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import (
|
from django.utils.translation import (
|
||||||
get_language, pgettext_lazy, ugettext_lazy as _,
|
get_language, gettext_lazy as _, pgettext_lazy,
|
||||||
)
|
)
|
||||||
from django_countries import countries
|
from django_countries import countries
|
||||||
from django_countries.fields import Country, CountryField
|
from django_countries.fields import Country, CountryField
|
||||||
@@ -41,6 +41,7 @@ from pretix.base.settings import (
|
|||||||
)
|
)
|
||||||
from pretix.base.templatetags.rich_text import rich_text
|
from pretix.base.templatetags.rich_text import rich_text
|
||||||
from pretix.control.forms import SplitDateTimeField
|
from pretix.control.forms import SplitDateTimeField
|
||||||
|
from pretix.helpers.countries import CachedCountries
|
||||||
from pretix.helpers.escapejson import escapejson_attr
|
from pretix.helpers.escapejson import escapejson_attr
|
||||||
from pretix.helpers.i18n import get_format_without_seconds
|
from pretix.helpers.i18n import get_format_without_seconds
|
||||||
from pretix.presale.signals import question_form_fields
|
from pretix.presale.signals import question_form_fields
|
||||||
@@ -214,6 +215,10 @@ def guess_country(event):
|
|||||||
return country
|
return country
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionCheckboxSelectMultiple(forms.CheckboxSelectMultiple):
|
||||||
|
option_template_name = 'pretixbase/forms/widgets/checkbox_option_with_links.html'
|
||||||
|
|
||||||
|
|
||||||
class BaseQuestionsForm(forms.Form):
|
class BaseQuestionsForm(forms.Form):
|
||||||
"""
|
"""
|
||||||
This form class is responsible for asking order-related questions. This includes
|
This form class is responsible for asking order-related questions. This includes
|
||||||
@@ -241,7 +246,7 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
if item.admission and event.settings.attendee_names_asked:
|
if item.admission and event.settings.attendee_names_asked:
|
||||||
self.fields['attendee_name_parts'] = NamePartsFormField(
|
self.fields['attendee_name_parts'] = NamePartsFormField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
required=event.settings.attendee_names_required,
|
required=event.settings.attendee_names_required and not self.all_optional,
|
||||||
scheme=event.settings.name_scheme,
|
scheme=event.settings.name_scheme,
|
||||||
titles=event.settings.name_scheme_titles,
|
titles=event.settings.name_scheme_titles,
|
||||||
label=_('Attendee name'),
|
label=_('Attendee name'),
|
||||||
@@ -249,7 +254,7 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
)
|
)
|
||||||
if item.admission and event.settings.attendee_emails_asked:
|
if item.admission and event.settings.attendee_emails_asked:
|
||||||
self.fields['attendee_email'] = forms.EmailField(
|
self.fields['attendee_email'] = forms.EmailField(
|
||||||
required=event.settings.attendee_emails_required,
|
required=event.settings.attendee_emails_required and not self.all_optional,
|
||||||
label=_('Attendee email'),
|
label=_('Attendee email'),
|
||||||
initial=(cartpos.attendee_email if cartpos else orderpos.attendee_email),
|
initial=(cartpos.attendee_email if cartpos else orderpos.attendee_email),
|
||||||
widget=forms.EmailInput(
|
widget=forms.EmailInput(
|
||||||
@@ -258,6 +263,75 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
if item.admission and event.settings.attendee_company_asked:
|
||||||
|
self.fields['company'] = forms.CharField(
|
||||||
|
required=event.settings.attendee_company_required and not self.all_optional,
|
||||||
|
label=_('Company'),
|
||||||
|
initial=(cartpos.company if cartpos else orderpos.company),
|
||||||
|
)
|
||||||
|
|
||||||
|
if item.admission and event.settings.attendee_addresses_asked:
|
||||||
|
self.fields['street'] = forms.CharField(
|
||||||
|
required=event.settings.attendee_addresses_required and not self.all_optional,
|
||||||
|
label=_('Address'),
|
||||||
|
widget=forms.Textarea(attrs={
|
||||||
|
'rows': 2,
|
||||||
|
'placeholder': _('Street and Number'),
|
||||||
|
'autocomplete': 'street-address'
|
||||||
|
}),
|
||||||
|
initial=(cartpos.street if cartpos else orderpos.street),
|
||||||
|
)
|
||||||
|
self.fields['zipcode'] = forms.CharField(
|
||||||
|
required=event.settings.attendee_addresses_required and not self.all_optional,
|
||||||
|
label=_('ZIP code'),
|
||||||
|
initial=(cartpos.zipcode if cartpos else orderpos.zipcode),
|
||||||
|
widget=forms.TextInput(attrs={
|
||||||
|
'autocomplete': 'postal-code',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
self.fields['city'] = forms.CharField(
|
||||||
|
required=event.settings.attendee_addresses_required and not self.all_optional,
|
||||||
|
label=_('City'),
|
||||||
|
initial=(cartpos.city if cartpos else orderpos.city),
|
||||||
|
widget=forms.TextInput(attrs={
|
||||||
|
'autocomplete': 'address-level2',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
country = (cartpos.country if cartpos else orderpos.country) or guess_country(event)
|
||||||
|
self.fields['country'] = CountryField(
|
||||||
|
countries=CachedCountries
|
||||||
|
).formfield(
|
||||||
|
required=event.settings.attendee_addresses_required and not self.all_optional,
|
||||||
|
label=_('Country'),
|
||||||
|
initial=country,
|
||||||
|
widget=forms.Select(attrs={
|
||||||
|
'autocomplete': 'country',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
c = [('', pgettext_lazy('address', 'Select state'))]
|
||||||
|
fprefix = str(self.prefix) + '-' if self.prefix is not None and self.prefix != '-' else ''
|
||||||
|
cc = None
|
||||||
|
if fprefix + 'country' in self.data:
|
||||||
|
cc = str(self.data[fprefix + 'country'])
|
||||||
|
elif country:
|
||||||
|
cc = str(country)
|
||||||
|
if cc and cc in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||||
|
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[cc]
|
||||||
|
statelist = [s for s in pycountry.subdivisions.get(country_code=cc) if s.type in types]
|
||||||
|
c += sorted([(s.code[3:], s.name) for s in statelist], key=lambda s: s[1])
|
||||||
|
elif fprefix + 'state' in self.data:
|
||||||
|
self.data = self.data.copy()
|
||||||
|
del self.data[fprefix + 'state']
|
||||||
|
|
||||||
|
self.fields['state'] = forms.ChoiceField(
|
||||||
|
label=pgettext_lazy('address', 'State'),
|
||||||
|
required=False,
|
||||||
|
choices=c,
|
||||||
|
widget=forms.Select(attrs={
|
||||||
|
'autocomplete': 'address-level1',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
self.fields['state'].widget.is_required = True
|
||||||
|
|
||||||
for q in questions:
|
for q in questions:
|
||||||
# Do we already have an answer? Provide it as the initial value
|
# Do we already have an answer? Provide it as the initial value
|
||||||
@@ -309,12 +383,14 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
initial=initial.answer if initial else None,
|
initial=initial.answer if initial else None,
|
||||||
)
|
)
|
||||||
elif q.type == Question.TYPE_COUNTRYCODE:
|
elif q.type == Question.TYPE_COUNTRYCODE:
|
||||||
field = CountryField().formfield(
|
field = CountryField(
|
||||||
|
countries=CachedCountries
|
||||||
|
).formfield(
|
||||||
label=label, required=required,
|
label=label, required=required,
|
||||||
help_text=help_text,
|
help_text=help_text,
|
||||||
widget=forms.Select,
|
widget=forms.Select,
|
||||||
empty_label='',
|
empty_label='',
|
||||||
initial=initial.answer if initial else None,
|
initial=initial.answer if initial else guess_country(event),
|
||||||
)
|
)
|
||||||
elif q.type == Question.TYPE_CHOICE:
|
elif q.type == Question.TYPE_CHOICE:
|
||||||
field = forms.ModelChoiceField(
|
field = forms.ModelChoiceField(
|
||||||
@@ -332,7 +408,7 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
label=label, required=required,
|
label=label, required=required,
|
||||||
help_text=help_text,
|
help_text=help_text,
|
||||||
to_field_name='identifier',
|
to_field_name='identifier',
|
||||||
widget=forms.CheckboxSelectMultiple,
|
widget=QuestionCheckboxSelectMultiple,
|
||||||
initial=initial.options.all() if initial else None,
|
initial=initial.options.all() if initial else None,
|
||||||
)
|
)
|
||||||
elif q.type == Question.TYPE_FILE:
|
elif q.type == Question.TYPE_FILE:
|
||||||
@@ -419,6 +495,10 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
d = super().clean()
|
d = super().clean()
|
||||||
|
|
||||||
|
if d.get('city') and d.get('country') and str(d['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||||
|
if not d.get('state'):
|
||||||
|
self.add_error('state', _('This field is required.'))
|
||||||
|
|
||||||
question_cache = {f.question.pk: f.question for f in self.fields.values() if getattr(f, 'question', None)}
|
question_cache = {f.question.pk: f.question for f in self.fields.values() if getattr(f, 'question', None)}
|
||||||
|
|
||||||
def question_is_visible(parentid, qvals):
|
def question_is_visible(parentid, qvals):
|
||||||
@@ -457,7 +537,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = InvoiceAddress
|
model = InvoiceAddress
|
||||||
fields = ('is_business', 'company', 'name_parts', 'street', 'zipcode', 'city', 'country', 'state',
|
fields = ('is_business', 'company', 'name_parts', 'street', 'zipcode', 'city', 'country', 'state',
|
||||||
'vat_id', 'internal_reference', 'beneficiary')
|
'vat_id', 'internal_reference', 'beneficiary', 'custom_field')
|
||||||
widgets = {
|
widgets = {
|
||||||
'is_business': BusinessBooleanRadio,
|
'is_business': BusinessBooleanRadio,
|
||||||
'street': forms.Textarea(attrs={
|
'street': forms.Textarea(attrs={
|
||||||
@@ -500,6 +580,8 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
if not event.settings.invoice_address_vatid:
|
if not event.settings.invoice_address_vatid:
|
||||||
del self.fields['vat_id']
|
del self.fields['vat_id']
|
||||||
|
|
||||||
|
self.fields['country'].choices = CachedCountries()
|
||||||
|
|
||||||
c = [('', pgettext_lazy('address', 'Select state'))]
|
c = [('', pgettext_lazy('address', 'Select state'))]
|
||||||
fprefix = self.prefix + '-' if self.prefix else ''
|
fprefix = self.prefix + '-' if self.prefix else ''
|
||||||
cc = None
|
cc = None
|
||||||
@@ -561,6 +643,11 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
if not event.settings.invoice_address_beneficiary:
|
if not event.settings.invoice_address_beneficiary:
|
||||||
del self.fields['beneficiary']
|
del self.fields['beneficiary']
|
||||||
|
|
||||||
|
if event.settings.invoice_address_custom_field:
|
||||||
|
self.fields['custom_field'].label = event.settings.invoice_address_custom_field
|
||||||
|
else:
|
||||||
|
del self.fields['custom_field']
|
||||||
|
|
||||||
for k, v in self.fields.items():
|
for k, v in self.fields.items():
|
||||||
if v.widget.attrs.get('autocomplete') or k == 'name_parts':
|
if v.widget.attrs.get('autocomplete') or k == 'name_parts':
|
||||||
v.widget.attrs['autocomplete'] = 'section-invoice billing ' + v.widget.attrs.get('autocomplete', '')
|
v.widget.attrs['autocomplete'] = 'section-invoice billing ' + v.widget.attrs.get('autocomplete', '')
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from django.contrib.auth.password_validation import (
|
|||||||
password_validators_help_texts, validate_password,
|
password_validators_help_texts, validate_password,
|
||||||
)
|
)
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from pytz import common_timezones
|
from pytz import common_timezones
|
||||||
|
|
||||||
from pretix.base.models import User
|
from pretix.base.models import User
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import re
|
|||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import BaseValidator
|
from django.core.validators import BaseValidator
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from i18nfield.strings import LazyI18nString
|
from i18nfield.strings import LazyI18nString
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,7 @@ from django import forms
|
|||||||
from django.utils.formats import get_format
|
from django.utils.formats import get_format
|
||||||
from django.utils.functional import lazy
|
from django.utils.functional import lazy
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.models import OrderPosition
|
|
||||||
from pretix.multidomain.urlreverse import eventreverse
|
|
||||||
|
|
||||||
|
|
||||||
class DatePickerWidget(forms.DateInput):
|
class DatePickerWidget(forms.DateInput):
|
||||||
@@ -71,6 +68,9 @@ class UploadedFileWidget(forms.ClearableFileInput):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self):
|
def url(self):
|
||||||
|
from pretix.base.models import OrderPosition
|
||||||
|
from pretix.multidomain.urlreverse import eventreverse
|
||||||
|
|
||||||
if isinstance(self.position, OrderPosition):
|
if isinstance(self.position, OrderPosition):
|
||||||
return eventreverse(self.event, 'presale:event.order.download.answer', kwargs={
|
return eventreverse(self.event, 'presale:event.order.download.answer', kwargs={
|
||||||
'order': self.position.order.code,
|
'order': self.position.order.code,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from contextlib import contextmanager
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import translation
|
from django.utils import translation
|
||||||
from django.utils.formats import date_format, number_format
|
from django.utils.formats import date_format, number_format
|
||||||
from django.utils.translation import ugettext
|
from django.utils.translation import gettext
|
||||||
from i18nfield.fields import ( # noqa
|
from i18nfield.fields import ( # noqa
|
||||||
I18nCharField, I18nTextarea, I18nTextField, I18nTextInput,
|
I18nCharField, I18nTextarea, I18nTextField, I18nTextInput,
|
||||||
)
|
)
|
||||||
@@ -69,6 +69,6 @@ class LazyLocaleException(Exception):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.msgargs:
|
if self.msgargs:
|
||||||
return ugettext(self.msg) % self.msgargs
|
return gettext(self.msg) % self.msgargs
|
||||||
else:
|
else:
|
||||||
return ugettext(self.msg)
|
return gettext(self.msg)
|
||||||
|
|||||||
@@ -10,7 +10,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 (
|
from django.utils.translation import (
|
||||||
get_language, pgettext, ugettext, ugettext_lazy,
|
get_language, gettext, gettext_lazy, pgettext,
|
||||||
)
|
)
|
||||||
from PIL.Image import BICUBIC
|
from PIL.Image import BICUBIC
|
||||||
from reportlab.lib import pagesizes
|
from reportlab.lib import pagesizes
|
||||||
@@ -28,7 +28,7 @@ from reportlab.platypus import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from pretix.base.decimal import round_decimal
|
from pretix.base.decimal import round_decimal
|
||||||
from pretix.base.models import Event, Invoice
|
from pretix.base.models import Event, Invoice, Order
|
||||||
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
|
||||||
|
|
||||||
@@ -264,7 +264,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
invoice_to_top = 52 * mm
|
invoice_to_top = 52 * mm
|
||||||
|
|
||||||
def _draw_invoice_to(self, canvas):
|
def _draw_invoice_to(self, canvas):
|
||||||
p = Paragraph(self.invoice.address_invoice_to.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
|
p = Paragraph(bleach.clean(self.invoice.address_invoice_to, tags=[]).strip().replace('\n', '<br />\n'),
|
||||||
|
style=self.stylesheet['Normal'])
|
||||||
p.wrapOn(canvas, self.invoice_to_width, self.invoice_to_height)
|
p.wrapOn(canvas, self.invoice_to_width, self.invoice_to_height)
|
||||||
p_size = p.wrap(self.invoice_to_width, self.invoice_to_height)
|
p_size = p.wrap(self.invoice_to_width, self.invoice_to_height)
|
||||||
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - p_size[1] - self.invoice_to_top)
|
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - p_size[1] - self.invoice_to_top)
|
||||||
@@ -422,7 +423,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
canvas.saveState()
|
canvas.saveState()
|
||||||
canvas.setFont('OpenSansBd', 30)
|
canvas.setFont('OpenSansBd', 30)
|
||||||
canvas.setFillColorRGB(32, 0, 0)
|
canvas.setFillColorRGB(32, 0, 0)
|
||||||
canvas.drawRightString(self.pagesize[0] - 20 * mm, (297 - 100) * mm, ugettext('TEST MODE'))
|
canvas.drawRightString(self.pagesize[0] - 20 * mm, (297 - 100) * mm, gettext('TEST MODE'))
|
||||||
canvas.restoreState()
|
canvas.restoreState()
|
||||||
|
|
||||||
def _on_first_page(self, canvas: Canvas, doc):
|
def _on_first_page(self, canvas: Canvas, doc):
|
||||||
@@ -459,6 +460,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
|
|
||||||
def _get_intro(self):
|
def _get_intro(self):
|
||||||
story = []
|
story = []
|
||||||
|
if self.invoice.custom_field:
|
||||||
|
story.append(Paragraph(
|
||||||
|
'{}: {}'.format(self.invoice.event.settings.invoice_address_custom_field, self.invoice.custom_field),
|
||||||
|
self.stylesheet['Normal']
|
||||||
|
))
|
||||||
|
|
||||||
if self.invoice.internal_reference:
|
if self.invoice.internal_reference:
|
||||||
story.append(Paragraph(
|
story.append(Paragraph(
|
||||||
pgettext('invoice', 'Customer reference: {reference}').format(reference=self.invoice.internal_reference),
|
pgettext('invoice', 'Customer reference: {reference}').format(reference=self.invoice.internal_reference),
|
||||||
@@ -559,6 +566,20 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
])
|
])
|
||||||
colwidths = [a * doc.width for a in (.65, .05, .30)]
|
colwidths = [a * doc.width for a in (.65, .05, .30)]
|
||||||
|
|
||||||
|
if self.invoice.event.settings.invoice_show_payments and not self.invoice.is_cancellation and \
|
||||||
|
self.invoice.order.status == Order.STATUS_PENDING:
|
||||||
|
pending_sum = self.invoice.order.pending_sum
|
||||||
|
if pending_sum != total:
|
||||||
|
tdata.append([pgettext('invoice', 'Received payments')] + (['', '', ''] if has_taxes else ['']) + [
|
||||||
|
money_filter(pending_sum - total, self.invoice.event.currency)
|
||||||
|
])
|
||||||
|
tdata.append([pgettext('invoice', 'Outstanding payments')] + (['', '', ''] if has_taxes else ['']) + [
|
||||||
|
money_filter(pending_sum, self.invoice.event.currency)
|
||||||
|
])
|
||||||
|
tstyledata += [
|
||||||
|
('FONTNAME', (0, len(tdata) - 3), (-1, len(tdata) - 3), self.font_bold),
|
||||||
|
]
|
||||||
|
|
||||||
table = Table(tdata, colWidths=colwidths, repeatRows=1)
|
table = Table(tdata, colWidths=colwidths, repeatRows=1)
|
||||||
table.setStyle(TableStyle(tstyledata))
|
table.setStyle(TableStyle(tstyledata))
|
||||||
story.append(table)
|
story.append(table)
|
||||||
@@ -667,7 +688,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
|
|
||||||
class Modern1Renderer(ClassicInvoiceRenderer):
|
class Modern1Renderer(ClassicInvoiceRenderer):
|
||||||
identifier = 'modern1'
|
identifier = 'modern1'
|
||||||
verbose_name = ugettext_lazy('Modern Invoice Renderer (pretix 2.7)')
|
verbose_name = gettext_lazy('Modern Invoice Renderer (pretix 2.7)')
|
||||||
bottom_margin = 16.9 * mm
|
bottom_margin = 16.9 * mm
|
||||||
top_margin = 16.9 * mm
|
top_margin = 16.9 * mm
|
||||||
right_margin = 20 * mm
|
right_margin = 20 * mm
|
||||||
|
|||||||
@@ -13,11 +13,17 @@ class Command(BaseCommand):
|
|||||||
return parser
|
return parser
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
|
try:
|
||||||
|
from django_extensions.management.commands import shell_plus # noqa
|
||||||
|
cmd = 'shell_plus'
|
||||||
|
except ImportError:
|
||||||
|
cmd = 'shell'
|
||||||
|
|
||||||
parser = self.create_parser(sys.argv[0], sys.argv[1])
|
parser = self.create_parser(sys.argv[0], sys.argv[1])
|
||||||
flags = parser.parse_known_args(sys.argv[2:])[1]
|
flags = parser.parse_known_args(sys.argv[2:])[1]
|
||||||
if "--override" in flags:
|
if "--override" in flags:
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
return call_command("shell_plus", *args, **options)
|
return call_command(cmd, *args, **options)
|
||||||
|
|
||||||
lookups = {}
|
lookups = {}
|
||||||
for flag in flags:
|
for flag in flags:
|
||||||
@@ -36,4 +42,4 @@ class Command(BaseCommand):
|
|||||||
for app_name, app_value in lookups.items()
|
for app_name, app_value in lookups.items()
|
||||||
}
|
}
|
||||||
with scope(**scope_options):
|
with scope(**scope_options):
|
||||||
return call_command("shell_plus", *args, **options)
|
return call_command(cmd, *args, **options)
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ from django.utils.translation.trans_real import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from pretix.base.settings import GlobalSettingsObject
|
from pretix.base.settings import GlobalSettingsObject
|
||||||
from pretix.multidomain.urlreverse import get_domain
|
from pretix.multidomain.urlreverse import (
|
||||||
|
get_event_domain, get_organizer_domain,
|
||||||
|
)
|
||||||
|
|
||||||
_supported = None
|
_supported = None
|
||||||
|
|
||||||
@@ -231,7 +233,10 @@ class SecurityMiddleware(MiddlewareMixin):
|
|||||||
dynamicdomain += " " + settings.SITE_URL
|
dynamicdomain += " " + settings.SITE_URL
|
||||||
|
|
||||||
if hasattr(request, 'organizer') and request.organizer:
|
if hasattr(request, 'organizer') and request.organizer:
|
||||||
domain = get_domain(request.organizer)
|
if hasattr(request, 'event') and request.event:
|
||||||
|
domain = get_event_domain(request.event, fallback=True)
|
||||||
|
else:
|
||||||
|
domain = get_organizer_domain(request.organizer)
|
||||||
if domain:
|
if domain:
|
||||||
siteurlsplit = urlsplit(settings.SITE_URL)
|
siteurlsplit = urlsplit(settings.SITE_URL)
|
||||||
if siteurlsplit.port and siteurlsplit.port not in (80, 443):
|
if siteurlsplit.port and siteurlsplit.port not in (80, 443):
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import django.core.validators
|
|||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
import pretix.base.validators
|
import pretix.base.validators
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from django.db import migrations, models
|
|||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
from django.db.models.functions import Concat
|
from django.db.models.functions import Concat
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
import pretix.base.models.auth
|
import pretix.base.models.auth
|
||||||
import pretix.base.validators
|
import pretix.base.validators
|
||||||
|
|||||||
28
src/pretix/base/migrations/0143_auto_20200217_1211.py
Normal file
28
src/pretix/base/migrations/0143_auto_20200217_1211.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 2.2.4 on 2020-02-17 12:11
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0142_auto_20191215_1522'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='seat',
|
||||||
|
name='row_label',
|
||||||
|
field=models.CharField(max_length=190, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='seat',
|
||||||
|
name='seat_label',
|
||||||
|
field=models.CharField(max_length=190, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='giftcard',
|
||||||
|
name='secret',
|
||||||
|
field=models.CharField(db_index=True, max_length=190),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 2.2.9 on 2020-02-18 08:06
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0143_auto_20200217_1211'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='invoiceaddress',
|
||||||
|
name='custom_field',
|
||||||
|
field=models.CharField(max_length=255, null=True, blank=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='invoice',
|
||||||
|
name='custom_field',
|
||||||
|
field=models.CharField(max_length=255, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
42
src/pretix/base/migrations/0145_auto_20200210_1038.py
Normal file
42
src/pretix/base/migrations/0145_auto_20200210_1038.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Generated by Django 2.2.8 on 2020-02-10 10:38
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import pretix.base.models.base
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0144_invoiceaddress_custom_field'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ItemMetaProperty',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||||
|
('name', models.CharField(db_index=True, max_length=50)),
|
||||||
|
('default', models.TextField()),
|
||||||
|
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='item_meta_properties', to='pretixbase.Event')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ItemMetaValue',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||||
|
('value', models.TextField()),
|
||||||
|
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='meta_values', to='pretixbase.Item')),
|
||||||
|
('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='item_values', to='pretixbase.ItemMetaProperty')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('item', 'property')},
|
||||||
|
},
|
||||||
|
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
src/pretix/base/migrations/0146_giftcardtransaction_text.py
Normal file
18
src/pretix/base/migrations/0146_giftcardtransaction_text.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 2.2.4 on 2020-03-02 11:28
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0145_auto_20200210_1038'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='giftcardtransaction',
|
||||||
|
name='text',
|
||||||
|
field=models.TextField(null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
20
src/pretix/base/migrations/0147_user_session_token.py
Normal file
20
src/pretix/base/migrations/0147_user_session_token.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 2.2.9 on 2020-03-21 15:12
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import pretix.base.models.auth
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0146_giftcardtransaction_text'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='session_token',
|
||||||
|
field=models.CharField(default=pretix.base.models.auth.generate_session_token, max_length=32),
|
||||||
|
),
|
||||||
|
]
|
||||||
24
src/pretix/base/migrations/0148_cancellationrequest.py
Normal file
24
src/pretix/base/migrations/0148_cancellationrequest.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 3.0.4 on 2020-03-25 10:05
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0147_user_session_token'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CancellationRequest',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('cancellation_fee', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||||
|
('refund_as_giftcard', models.BooleanField(default=False)),
|
||||||
|
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cancellation_requests', to='pretixbase.Order')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
46
src/pretix/base/migrations/0149_order_cancellation_date.py
Normal file
46
src/pretix/base/migrations/0149_order_cancellation_date.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Generated by Django 3.0.4 on 2020-03-25 14:40
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.db.models import Count, OuterRef, Q, Subquery
|
||||||
|
from django.utils.timezone import now
|
||||||
|
|
||||||
|
|
||||||
|
def fill_cancellation_date(apps, schema_editor):
|
||||||
|
Order = apps.get_model('pretixbase', 'Order')
|
||||||
|
LogEntry = apps.get_model('pretixbase', 'LogEntry')
|
||||||
|
OrderPosition = apps.get_model('pretixbase', 'OrderPosition')
|
||||||
|
|
||||||
|
s = OrderPosition.all.filter(
|
||||||
|
order=OuterRef('pk'),
|
||||||
|
canceled=False,
|
||||||
|
).order_by().values('order').annotate(k=Count('id')).values('k')
|
||||||
|
for o in Order.objects.annotate(
|
||||||
|
pcnt=Subquery(s)
|
||||||
|
).filter(
|
||||||
|
Q(pcnt=0) | Q(pcnt__isnull=True) | Q(status="c")
|
||||||
|
).values('id').iterator():
|
||||||
|
le = LogEntry.objects.filter(
|
||||||
|
content_type__model="order",
|
||||||
|
object_id=o['id'],
|
||||||
|
action_type='pretix.event.order.canceled'
|
||||||
|
).order_by('-datetime').only('datetime').first()
|
||||||
|
if le:
|
||||||
|
Order.objects.filter(pk=o['id']).update(
|
||||||
|
cancellation_date=le.datetime,
|
||||||
|
last_modified=now()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0148_cancellationrequest'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='order',
|
||||||
|
name='cancellation_date',
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.RunPython(fill_cancellation_date, migrations.RunPython.noop)
|
||||||
|
]
|
||||||
74
src/pretix/base/migrations/0150_auto_20200401_1123.py
Normal file
74
src/pretix/base/migrations/0150_auto_20200401_1123.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# Generated by Django 3.0.4 on 2020-04-01 11:24
|
||||||
|
|
||||||
|
import django_countries.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0149_order_cancellation_date'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='cartposition',
|
||||||
|
name='city',
|
||||||
|
field=models.CharField(max_length=255, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='cartposition',
|
||||||
|
name='company',
|
||||||
|
field=models.CharField(max_length=255, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='cartposition',
|
||||||
|
name='country',
|
||||||
|
field=django_countries.fields.CountryField(max_length=2, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='cartposition',
|
||||||
|
name='state',
|
||||||
|
field=models.CharField(max_length=255, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='cartposition',
|
||||||
|
name='street',
|
||||||
|
field=models.TextField(null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='cartposition',
|
||||||
|
name='zipcode',
|
||||||
|
field=models.CharField(max_length=30, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='orderposition',
|
||||||
|
name='city',
|
||||||
|
field=models.CharField(max_length=255, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='orderposition',
|
||||||
|
name='company',
|
||||||
|
field=models.CharField(max_length=255, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='orderposition',
|
||||||
|
name='country',
|
||||||
|
field=django_countries.fields.CountryField(max_length=2, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='orderposition',
|
||||||
|
name='state',
|
||||||
|
field=models.CharField(max_length=255, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='orderposition',
|
||||||
|
name='street',
|
||||||
|
field=models.TextField(null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='orderposition',
|
||||||
|
name='zipcode',
|
||||||
|
field=models.CharField(max_length=30, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -10,9 +10,9 @@ from .event import (
|
|||||||
from .giftcards import GiftCard, GiftCardAcceptance, GiftCardTransaction
|
from .giftcards import GiftCard, GiftCardAcceptance, GiftCardTransaction
|
||||||
from .invoices import Invoice, InvoiceLine, invoice_filename
|
from .invoices import Invoice, InvoiceLine, invoice_filename
|
||||||
from .items import (
|
from .items import (
|
||||||
Item, ItemAddOn, ItemBundle, ItemCategory, ItemVariation, Question,
|
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaProperty, ItemMetaValue,
|
||||||
QuestionOption, Quota, SubEventItem, SubEventItemVariation,
|
ItemVariation, Question, QuestionOption, Quota, SubEventItem,
|
||||||
itempicture_upload_to,
|
SubEventItemVariation, itempicture_upload_to,
|
||||||
)
|
)
|
||||||
from .log import LogEntry
|
from .log import LogEntry
|
||||||
from .notifications import NotificationSetting
|
from .notifications import NotificationSetting
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ from django.contrib.auth.tokens import default_token_generator
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string, salted_hmac
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_otp.models import Device
|
from django_otp.models import Device
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
from u2flib_server.utils import (
|
from u2flib_server.utils import (
|
||||||
@@ -54,6 +54,10 @@ def generate_notifications_token():
|
|||||||
return get_random_string(length=32)
|
return get_random_string(length=32)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_session_token():
|
||||||
|
return get_random_string(length=32)
|
||||||
|
|
||||||
|
|
||||||
class SuperuserPermissionSet:
|
class SuperuserPermissionSet:
|
||||||
def __contains__(self, item):
|
def __contains__(self, item):
|
||||||
return True
|
return True
|
||||||
@@ -110,6 +114,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
)
|
)
|
||||||
notifications_token = models.CharField(max_length=255, default=generate_notifications_token)
|
notifications_token = models.CharField(max_length=255, default=generate_notifications_token)
|
||||||
auth_backend = models.CharField(max_length=255, default='native')
|
auth_backend = models.CharField(max_length=255, default='native')
|
||||||
|
session_token = models.CharField(max_length=32, default=generate_session_token)
|
||||||
|
|
||||||
objects = UserManager()
|
objects = UserManager()
|
||||||
|
|
||||||
@@ -382,6 +387,20 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
self._staff_session_cache[session_key] = sess
|
self._staff_session_cache[session_key] = sess
|
||||||
return self._staff_session_cache[session_key]
|
return self._staff_session_cache[session_key]
|
||||||
|
|
||||||
|
def get_session_auth_hash(self):
|
||||||
|
"""
|
||||||
|
Return an HMAC that needs to
|
||||||
|
"""
|
||||||
|
key_salt = "pretix.base.models.User.get_session_auth_hash"
|
||||||
|
payload = self.password
|
||||||
|
payload += self.email
|
||||||
|
payload += self.session_token
|
||||||
|
return salted_hmac(key_salt, payload).hexdigest()
|
||||||
|
|
||||||
|
def update_session_token(self):
|
||||||
|
self.session_token = generate_session_token()
|
||||||
|
self.save(update_fields=['session_token'])
|
||||||
|
|
||||||
|
|
||||||
class StaffSession(models.Model):
|
class StaffSession(models.Model):
|
||||||
user = models.ForeignKey('User', on_delete=models.PROTECT)
|
user = models.ForeignKey('User', on_delete=models.PROTECT)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Exists, OuterRef
|
from django.db.models import Exists, OuterRef
|
||||||
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 gettext_lazy as _, pgettext_lazy
|
||||||
from django_scopes import ScopedManager
|
from django_scopes import ScopedManager
|
||||||
|
|
||||||
from pretix.base.models import LoggedModel
|
from pretix.base.models import LoggedModel
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import string
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Max
|
from django.db.models import Max
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_scopes import ScopedManager, scopes_disabled
|
from django_scopes import ScopedManager, scopes_disabled
|
||||||
|
|
||||||
from pretix.base.models import LoggedModel
|
from pretix.base.models import LoggedModel
|
||||||
@@ -94,6 +94,7 @@ class Device(LoggedModel):
|
|||||||
return {
|
return {
|
||||||
'can_view_orders',
|
'can_view_orders',
|
||||||
'can_change_orders',
|
'can_change_orders',
|
||||||
|
'can_manage_gift_cards'
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_event_permission_set(self, organizer, event) -> set:
|
def get_event_permission_set(self, organizer, event) -> set:
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ from django.db import models
|
|||||||
from django.db.models import Exists, F, OuterRef, Prefetch, Q, Subquery
|
from django.db.models import Exists, F, OuterRef, Prefetch, Q, Subquery
|
||||||
from django.template.defaultfilters import date as _date
|
from django.template.defaultfilters import date as _date
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
|
from django.utils.formats import date_format
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.timezone import make_aware, now
|
from django.utils.timezone import make_aware, now
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_scopes import ScopedManager, scopes_disabled
|
from django_scopes import ScopedManager, scopes_disabled
|
||||||
from i18nfield.fields import I18nCharField, I18nTextField
|
from i18nfield.fields import I18nCharField, I18nTextField
|
||||||
|
|
||||||
@@ -293,7 +294,7 @@ class Event(EventMixin, LoggedModel):
|
|||||||
"This will be used in URLs, order codes, invoice numbers, and bank transfer references."),
|
"This will be used in URLs, order codes, invoice numbers, and bank transfer references."),
|
||||||
validators=[
|
validators=[
|
||||||
RegexValidator(
|
RegexValidator(
|
||||||
regex="^[a-zA-Z0-9.-]+$",
|
regex="^[a-zA-Z0-9][a-zA-Z0-9.-]*$",
|
||||||
message=_("The slug may only contain letters, numbers, dots and dashes."),
|
message=_("The slug may only contain letters, numbers, dots and dashes."),
|
||||||
),
|
),
|
||||||
EventSlugBanlistValidator()
|
EventSlugBanlistValidator()
|
||||||
@@ -370,6 +371,8 @@ class Event(EventMixin, LoggedModel):
|
|||||||
"""
|
"""
|
||||||
self.settings.invoice_renderer = 'modern1'
|
self.settings.invoice_renderer = 'modern1'
|
||||||
self.settings.invoice_include_expire_date = True
|
self.settings.invoice_include_expire_date = True
|
||||||
|
self.settings.ticketoutput_pdf__enabled = True
|
||||||
|
self.settings.ticketoutput_passbook__enabled = True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def social_image(self):
|
def social_image(self):
|
||||||
@@ -385,7 +388,7 @@ class Event(EventMixin, LoggedModel):
|
|||||||
if img:
|
if img:
|
||||||
return urljoin(build_absolute_uri(self, 'presale:event.index'), img)
|
return urljoin(build_absolute_uri(self, 'presale:event.index'), img)
|
||||||
|
|
||||||
def free_seats(self, ignore_voucher=None, sales_channel='web'):
|
def free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False):
|
||||||
from .orders import CartPosition, Order, OrderPosition
|
from .orders import CartPosition, Order, OrderPosition
|
||||||
from .vouchers import Voucher
|
from .vouchers import Voucher
|
||||||
vqs = Voucher.objects.filter(
|
vqs = Voucher.objects.filter(
|
||||||
@@ -416,7 +419,7 @@ class Event(EventMixin, LoggedModel):
|
|||||||
vqs
|
vqs
|
||||||
)
|
)
|
||||||
).filter(has_order=False, has_cart=False, has_voucher=False)
|
).filter(has_order=False, has_cart=False, has_voucher=False)
|
||||||
if sales_channel not in self.settings.seating_allow_blocked_seats_for_channel:
|
if not (sales_channel in self.settings.seating_allow_blocked_seats_for_channel or include_blocked):
|
||||||
qs = qs.filter(blocked=False)
|
qs = qs.filter(blocked=False)
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
@@ -515,7 +518,7 @@ class Event(EventMixin, LoggedModel):
|
|||||||
), tz)
|
), tz)
|
||||||
|
|
||||||
def copy_data_from(self, other):
|
def copy_data_from(self, other):
|
||||||
from . import ItemAddOn, ItemCategory, Item, Question, Quota
|
from . import ItemAddOn, ItemCategory, Item, Question, Quota, ItemMetaValue
|
||||||
from ..signals import event_copy_data
|
from ..signals import event_copy_data
|
||||||
|
|
||||||
self.plugins = other.plugins
|
self.plugins = other.plugins
|
||||||
@@ -540,6 +543,14 @@ class Event(EventMixin, LoggedModel):
|
|||||||
c.save()
|
c.save()
|
||||||
c.log_action('pretix.object.cloned')
|
c.log_action('pretix.object.cloned')
|
||||||
|
|
||||||
|
item_meta_properties_map = {}
|
||||||
|
for imp in other.item_meta_properties.all():
|
||||||
|
item_meta_properties_map[imp.pk] = imp
|
||||||
|
imp.pk = None
|
||||||
|
imp.event = self
|
||||||
|
imp.save()
|
||||||
|
imp.log_action('pretix.object.cloned')
|
||||||
|
|
||||||
item_map = {}
|
item_map = {}
|
||||||
variation_map = {}
|
variation_map = {}
|
||||||
for i in Item.objects.filter(event=other).prefetch_related('variations'):
|
for i in Item.objects.filter(event=other).prefetch_related('variations'):
|
||||||
@@ -561,6 +572,12 @@ class Event(EventMixin, LoggedModel):
|
|||||||
v.item = i
|
v.item = i
|
||||||
v.save()
|
v.save()
|
||||||
|
|
||||||
|
for imv in ItemMetaValue.objects.filter(item__event=other).prefetch_related('item', 'property'):
|
||||||
|
imv.pk = None
|
||||||
|
imv.property = item_meta_properties_map[imv.property.pk]
|
||||||
|
imv.item = item_map[imv.item.pk]
|
||||||
|
imv.save()
|
||||||
|
|
||||||
for ia in ItemAddOn.objects.filter(base_item__event=other).prefetch_related('base_item', 'addon_category'):
|
for ia in ItemAddOn.objects.filter(base_item__event=other).prefetch_related('base_item', 'addon_category'):
|
||||||
ia.pk = None
|
ia.pk = None
|
||||||
ia.base_item = item_map[ia.base_item.pk]
|
ia.base_item = item_map[ia.base_item.pk]
|
||||||
@@ -608,8 +625,10 @@ class Event(EventMixin, LoggedModel):
|
|||||||
q.dependency_question = question_map[q.dependency_question_id]
|
q.dependency_question = question_map[q.dependency_question_id]
|
||||||
q.save(update_fields=['dependency_question'])
|
q.save(update_fields=['dependency_question'])
|
||||||
|
|
||||||
|
checkin_list_map = {}
|
||||||
for cl in other.checkin_lists.filter(subevent__isnull=True).prefetch_related('limit_products'):
|
for cl in other.checkin_lists.filter(subevent__isnull=True).prefetch_related('limit_products'):
|
||||||
items = list(cl.limit_products.all())
|
items = list(cl.limit_products.all())
|
||||||
|
checkin_list_map[cl.pk] = cl
|
||||||
cl.pk = None
|
cl.pk = None
|
||||||
cl.event = self
|
cl.event = self
|
||||||
cl.save()
|
cl.save()
|
||||||
@@ -633,6 +652,8 @@ class Event(EventMixin, LoggedModel):
|
|||||||
for s in other.seats.filter(subevent__isnull=True):
|
for s in other.seats.filter(subevent__isnull=True):
|
||||||
s.pk = None
|
s.pk = None
|
||||||
s.event = self
|
s.event = self
|
||||||
|
if s.product_id:
|
||||||
|
s.product = item_map[s.product_id]
|
||||||
s.save()
|
s.save()
|
||||||
|
|
||||||
for s in other.settings._objects.all():
|
for s in other.settings._objects.all():
|
||||||
@@ -662,7 +683,7 @@ class Event(EventMixin, LoggedModel):
|
|||||||
event_copy_data.send(
|
event_copy_data.send(
|
||||||
sender=self, other=other,
|
sender=self, other=other,
|
||||||
tax_map=tax_map, category_map=category_map, item_map=item_map, variation_map=variation_map,
|
tax_map=tax_map, category_map=category_map, item_map=item_map, variation_map=variation_map,
|
||||||
question_map=question_map
|
question_map=question_map, checkin_list_map=checkin_list_map
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_payment_providers(self, cached=False) -> dict:
|
def get_payment_providers(self, cached=False) -> dict:
|
||||||
@@ -1014,9 +1035,13 @@ class SubEvent(EventMixin, LoggedModel):
|
|||||||
ordering = ("date_from", "name")
|
ordering = ("date_from", "name")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '{} - {}'.format(self.name, self.get_date_range_display())
|
return '{} - {} {}'.format(
|
||||||
|
self.name,
|
||||||
|
self.get_date_range_display(),
|
||||||
|
date_format(self.date_from.astimezone(self.timezone), "TIME_FORMAT") if self.settings.show_times else ""
|
||||||
|
).strip()
|
||||||
|
|
||||||
def free_seats(self, ignore_voucher=None, sales_channel='web'):
|
def free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False):
|
||||||
from .orders import CartPosition, Order, OrderPosition
|
from .orders import CartPosition, Order, OrderPosition
|
||||||
from .vouchers import Voucher
|
from .vouchers import Voucher
|
||||||
vqs = Voucher.objects.filter(
|
vqs = Voucher.objects.filter(
|
||||||
@@ -1050,7 +1075,7 @@ class SubEvent(EventMixin, LoggedModel):
|
|||||||
vqs
|
vqs
|
||||||
)
|
)
|
||||||
).filter(has_order=False, has_cart=False, has_voucher=False)
|
).filter(has_order=False, has_cart=False, has_voucher=False)
|
||||||
if sales_channel not in self.settings.seating_allow_blocked_seats_for_channel:
|
if not (sales_channel in self.settings.seating_allow_blocked_seats_for_channel or include_blocked):
|
||||||
qs = qs.filter(blocked=False)
|
qs = qs.filter(blocked=False)
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.validators import RegexValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Sum
|
from django.db.models import Sum
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.banlist import banned
|
from pretix.base.banlist import banned
|
||||||
from pretix.base.models import LoggedModel
|
from pretix.base.models import LoggedModel
|
||||||
|
|
||||||
|
|
||||||
def gen_giftcard_secret(length):
|
def gen_giftcard_secret(length=8):
|
||||||
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
|
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
|
||||||
while True:
|
while True:
|
||||||
code = get_random_string(length=length, allowed_chars=charset)
|
code = get_random_string(length=length, allowed_chars=charset)
|
||||||
@@ -50,6 +51,12 @@ class GiftCard(LoggedModel):
|
|||||||
max_length=190,
|
max_length=190,
|
||||||
db_index=True,
|
db_index=True,
|
||||||
verbose_name=_('Gift card code'),
|
verbose_name=_('Gift card code'),
|
||||||
|
validators=[
|
||||||
|
RegexValidator(
|
||||||
|
regex="^[a-zA-Z0-9][a-zA-Z0-9.-]+$",
|
||||||
|
message=_("The giftcard code may only contain letters, numbers, dots and dashes."),
|
||||||
|
)
|
||||||
|
],
|
||||||
)
|
)
|
||||||
testmode = models.BooleanField(
|
testmode = models.BooleanField(
|
||||||
verbose_name=_('Test mode card'),
|
verbose_name=_('Test mode card'),
|
||||||
@@ -76,6 +83,7 @@ class GiftCard(LoggedModel):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = (('secret', 'issuer'),)
|
unique_together = (('secret', 'issuer'),)
|
||||||
|
ordering = ("issuance",)
|
||||||
|
|
||||||
|
|
||||||
class GiftCardTransaction(models.Model):
|
class GiftCardTransaction(models.Model):
|
||||||
@@ -112,6 +120,7 @@ class GiftCardTransaction(models.Model):
|
|||||||
blank=True,
|
blank=True,
|
||||||
on_delete=models.PROTECT
|
on_delete=models.PROTECT
|
||||||
)
|
)
|
||||||
|
text = models.TextField(blank=True, null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ("datetime",)
|
ordering = ("datetime",)
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ class Invoice(models.Model):
|
|||||||
|
|
||||||
file = models.FileField(null=True, blank=True, upload_to=invoice_filename, max_length=255)
|
file = models.FileField(null=True, blank=True, upload_to=invoice_filename, max_length=255)
|
||||||
internal_reference = models.TextField(blank=True)
|
internal_reference = models.TextField(blank=True)
|
||||||
|
custom_field = models.CharField(max_length=255, null=True)
|
||||||
|
|
||||||
objects = ScopedManager(organizer='event__organizer')
|
objects = ScopedManager(organizer='event__organizer')
|
||||||
|
|
||||||
@@ -120,13 +121,19 @@ class Invoice(models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def full_invoice_from(self):
|
def full_invoice_from(self):
|
||||||
|
taxidrow = ""
|
||||||
|
if self.invoice_from_tax_id:
|
||||||
|
if str(self.invoice_from_country) == "AU":
|
||||||
|
taxidrow = "ABN: %s" % self.invoice_from_tax_id
|
||||||
|
else:
|
||||||
|
taxidrow = pgettext("invoice", "Tax ID: %s") % self.invoice_from_tax_id
|
||||||
parts = [
|
parts = [
|
||||||
self.invoice_from_name,
|
self.invoice_from_name,
|
||||||
self.invoice_from,
|
self.invoice_from,
|
||||||
(self.invoice_from_zipcode or "") + " " + (self.invoice_from_city or ""),
|
(self.invoice_from_zipcode or "") + " " + (self.invoice_from_city or ""),
|
||||||
self.invoice_from_country.name if self.invoice_from_country else "",
|
self.invoice_from_country.name if self.invoice_from_country else "",
|
||||||
pgettext("invoice", "VAT-ID: %s") % self.invoice_from_vat_id if self.invoice_from_vat_id else "",
|
pgettext("invoice", "VAT-ID: %s") % self.invoice_from_vat_id if self.invoice_from_vat_id else "",
|
||||||
pgettext("invoice", "Tax ID: %s") % self.invoice_from_tax_id if self.invoice_from_tax_id else "",
|
taxidrow,
|
||||||
]
|
]
|
||||||
return '\n'.join([p.strip() for p in parts if p and p.strip()])
|
return '\n'.join([p.strip() for p in parts if p and p.strip()])
|
||||||
|
|
||||||
@@ -150,9 +157,12 @@ class Invoice(models.Model):
|
|||||||
state_name = self.invoice_to_state
|
state_name = self.invoice_to_state
|
||||||
if str(self.invoice_to_country) in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
if str(self.invoice_to_country) in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||||
if COUNTRIES_WITH_STATE_IN_ADDRESS[str(self.invoice_to_country)][1] == 'long':
|
if COUNTRIES_WITH_STATE_IN_ADDRESS[str(self.invoice_to_country)][1] == 'long':
|
||||||
state_name = pycountry.subdivisions.get(
|
try:
|
||||||
code='{}-{}'.format(self.invoice_to_country, self.invoice_to_state)
|
state_name = pycountry.subdivisions.get(
|
||||||
).name
|
code='{}-{}'.format(self.invoice_to_country, self.invoice_to_state)
|
||||||
|
).name
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
parts = [
|
parts = [
|
||||||
self.invoice_to_company,
|
self.invoice_to_company,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import sys
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
from collections import Counter
|
from collections import Counter, OrderedDict
|
||||||
from datetime import date, datetime, time
|
from datetime import date, datetime, time
|
||||||
from decimal import Decimal, DecimalException
|
from decimal import Decimal, DecimalException
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
@@ -9,13 +9,14 @@ import dateutil.parser
|
|||||||
import pytz
|
import pytz
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.core.validators import RegexValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import F, Func, Q, Sum
|
from django.db.models import F, Func, Q, Sum
|
||||||
from django.utils import formats
|
from django.utils import formats
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.timezone import is_naive, make_aware, now
|
from django.utils.timezone import is_naive, make_aware, now
|
||||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
from django_countries.fields import Country
|
from django_countries.fields import Country
|
||||||
from django_scopes import ScopedManager
|
from django_scopes import ScopedManager
|
||||||
from i18nfield.fields import I18nCharField, I18nTextField
|
from i18nfield.fields import I18nCharField, I18nTextField
|
||||||
@@ -454,7 +455,8 @@ class Item(LoggedModel):
|
|||||||
t = TaxedPrice(gross=price, net=price, tax=Decimal('0.00'),
|
t = TaxedPrice(gross=price, net=price, tax=Decimal('0.00'),
|
||||||
rate=Decimal('0.00'), name='')
|
rate=Decimal('0.00'), name='')
|
||||||
else:
|
else:
|
||||||
t = self.tax_rule.tax(price, base_price_is=base_price_is, currency=currency)
|
t = self.tax_rule.tax(price, base_price_is=base_price_is,
|
||||||
|
currency=currency or self.event.currency)
|
||||||
|
|
||||||
if include_bundled:
|
if include_bundled:
|
||||||
for b in self.bundles.all():
|
for b in self.bundles.all():
|
||||||
@@ -591,6 +593,16 @@ class Item(LoggedModel):
|
|||||||
if from_date > until_date:
|
if from_date > until_date:
|
||||||
raise ValidationError(_('The item\'s availability cannot end before it starts.'))
|
raise ValidationError(_('The item\'s availability cannot end before it starts.'))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def meta_data(self):
|
||||||
|
data = {p.name: p.default for p in self.event.item_meta_properties.all()}
|
||||||
|
if hasattr(self, 'meta_values_cached'):
|
||||||
|
data.update({v.property.name: v.value for v in self.meta_values_cached})
|
||||||
|
else:
|
||||||
|
data.update({v.property.name: v.value for v in self.meta_values.select_related('property').all()})
|
||||||
|
|
||||||
|
return OrderedDict((k, v) for k, v in sorted(data.items(), key=lambda k: k[0]))
|
||||||
|
|
||||||
|
|
||||||
class ItemVariation(models.Model):
|
class ItemVariation(models.Model):
|
||||||
"""
|
"""
|
||||||
@@ -1105,10 +1117,13 @@ class Question(LoggedModel):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
if self.type == Question.TYPE_CHOICE:
|
if self.type == Question.TYPE_CHOICE:
|
||||||
try:
|
q = Q(identifier=answer)
|
||||||
return self.options.get(Q(pk=answer) | Q(identifier=answer))
|
if isinstance(answer, int) or answer.isdigit():
|
||||||
except:
|
q |= Q(pk=answer)
|
||||||
|
o = self.options.filter(q).first()
|
||||||
|
if not o:
|
||||||
raise ValidationError(_('Invalid option selected.'))
|
raise ValidationError(_('Invalid option selected.'))
|
||||||
|
return o
|
||||||
elif self.type == Question.TYPE_CHOICE_MULTIPLE:
|
elif self.type == Question.TYPE_CHOICE_MULTIPLE:
|
||||||
if isinstance(answer, str):
|
if isinstance(answer, str):
|
||||||
l_ = list(self.options.filter(
|
l_ = list(self.options.filter(
|
||||||
@@ -1541,3 +1556,57 @@ class Quota(LoggedModel):
|
|||||||
else:
|
else:
|
||||||
if subevent:
|
if subevent:
|
||||||
raise ValidationError(_('The subevent does not belong to this event.'))
|
raise ValidationError(_('The subevent does not belong to this event.'))
|
||||||
|
|
||||||
|
|
||||||
|
class ItemMetaProperty(LoggedModel):
|
||||||
|
"""
|
||||||
|
An event can have ItemMetaProperty objects attached to define meta information fields
|
||||||
|
for its items. This information can be re-used for example in ticket layouts.
|
||||||
|
|
||||||
|
:param event: The event this property is defined for.
|
||||||
|
:type event: Event
|
||||||
|
:param name: Name
|
||||||
|
:type name: Name of the property, used in various places
|
||||||
|
:param default: Default value
|
||||||
|
:type default: str
|
||||||
|
"""
|
||||||
|
event = models.ForeignKey(Event, related_name="item_meta_properties", on_delete=models.CASCADE)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=50, db_index=True,
|
||||||
|
help_text=_(
|
||||||
|
"Can not contain spaces or special characters except underscores"
|
||||||
|
),
|
||||||
|
validators=[
|
||||||
|
RegexValidator(
|
||||||
|
regex="^[a-zA-Z0-9_]+$",
|
||||||
|
message=_("The property name may only contain letters, numbers and underscores."),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
verbose_name=_("Name"),
|
||||||
|
)
|
||||||
|
default = models.TextField(blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ItemMetaValue(LoggedModel):
|
||||||
|
"""
|
||||||
|
A meta-data value assigned to an item.
|
||||||
|
|
||||||
|
:param item: The item this metadata is valid for
|
||||||
|
:type item: Item
|
||||||
|
:param property: The property this value belongs to
|
||||||
|
:type property: ItemMetaProperty
|
||||||
|
:param value: The actual value
|
||||||
|
:type value: str
|
||||||
|
"""
|
||||||
|
item = models.ForeignKey('Item', on_delete=models.CASCADE, related_name='meta_values')
|
||||||
|
property = models.ForeignKey('ItemMetaProperty', on_delete=models.CASCADE, related_name='item_values')
|
||||||
|
value = models.TextField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('item', 'property')
|
||||||
|
|
||||||
|
def delete(self, *args, **kwargs):
|
||||||
|
super().delete(*args, **kwargs)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from django.db import models
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
|
|
||||||
from pretix.base.signals import logentry_object_link
|
from pretix.base.signals import logentry_object_link
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class NotificationSetting(models.Model):
|
class NotificationSetting(models.Model):
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import string
|
import string
|
||||||
|
from collections import Counter
|
||||||
from datetime import datetime, time, timedelta
|
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
|
||||||
@@ -25,7 +26,7 @@ from django.utils.encoding import escape_uri_path
|
|||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.timezone import make_aware, now
|
from django.utils.timezone import make_aware, now
|
||||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
from django_countries.fields import Country, CountryField
|
from django_countries.fields import Country, CountryField
|
||||||
from django_scopes import ScopedManager, scopes_disabled
|
from django_scopes import ScopedManager, scopes_disabled
|
||||||
from i18nfield.strings import LazyI18nString
|
from i18nfield.strings import LazyI18nString
|
||||||
@@ -43,6 +44,7 @@ from pretix.base.services.locking import NoLockManager
|
|||||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||||
from pretix.base.signals import order_gracefully_delete
|
from pretix.base.signals import order_gracefully_delete
|
||||||
|
|
||||||
|
from ...helpers.countries import CachedCountries
|
||||||
from .base import LockModel, LoggedModel
|
from .base import LockModel, LoggedModel
|
||||||
from .event import Event, SubEvent
|
from .event import Event, SubEvent
|
||||||
from .items import Item, ItemVariation, Question, QuestionOption, Quota
|
from .items import Item, ItemVariation, Question, QuestionOption, Quota
|
||||||
@@ -151,6 +153,9 @@ class Order(LockModel, LoggedModel):
|
|||||||
datetime = models.DateTimeField(
|
datetime = models.DateTimeField(
|
||||||
verbose_name=_("Date"), db_index=True
|
verbose_name=_("Date"), db_index=True
|
||||||
)
|
)
|
||||||
|
cancellation_date = models.DateTimeField(
|
||||||
|
null=True, blank=True
|
||||||
|
)
|
||||||
expires = models.DateTimeField(
|
expires = models.DateTimeField(
|
||||||
verbose_name=_("Expiration date")
|
verbose_name=_("Expiration date")
|
||||||
)
|
)
|
||||||
@@ -400,10 +405,13 @@ class Order(LockModel, LoggedModel):
|
|||||||
term_last = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
|
term_last = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
|
||||||
if term_last:
|
if term_last:
|
||||||
if self.event.has_subevents and subevents:
|
if self.event.has_subevents and subevents:
|
||||||
term_last = min([
|
terms = [
|
||||||
term_last.datetime(se).date()
|
term_last.datetime(se).date()
|
||||||
for se in subevents
|
for se in subevents
|
||||||
])
|
]
|
||||||
|
if not terms:
|
||||||
|
return
|
||||||
|
term_last = min(terms)
|
||||||
else:
|
else:
|
||||||
term_last = term_last.datetime(self.event).date()
|
term_last = term_last.datetime(self.event).date()
|
||||||
term_last = make_aware(datetime.combine(
|
term_last = make_aware(datetime.combine(
|
||||||
@@ -423,7 +431,7 @@ class Order(LockModel, LoggedModel):
|
|||||||
|
|
||||||
def cancel_allowed(self):
|
def cancel_allowed(self):
|
||||||
return (
|
return (
|
||||||
self.status in (Order.STATUS_PENDING, Order.STATUS_PAID) and self.count_positions
|
self.status in (Order.STATUS_PENDING, Order.STATUS_PAID, Order.STATUS_EXPIRED) and self.count_positions
|
||||||
)
|
)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
@@ -434,26 +442,28 @@ class Order(LockModel, LoggedModel):
|
|||||||
until = self.event.settings.get('cancel_allow_user_until', as_type=RelativeDateWrapper)
|
until = self.event.settings.get('cancel_allow_user_until', as_type=RelativeDateWrapper)
|
||||||
if until:
|
if until:
|
||||||
if self.event.has_subevents:
|
if self.event.has_subevents:
|
||||||
return min([
|
terms = [
|
||||||
until.datetime(se)
|
until.datetime(se)
|
||||||
for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True))
|
for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True))
|
||||||
])
|
]
|
||||||
|
return min(terms) if terms else None
|
||||||
else:
|
else:
|
||||||
return until.datetime(self.event)
|
return until.datetime(self.event)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def user_cancel_fee(self):
|
def user_cancel_fee(self):
|
||||||
fee = Decimal('0.00')
|
fee = Decimal('0.00')
|
||||||
if self.event.settings.cancel_allow_user_paid_keep:
|
|
||||||
fee += self.event.settings.cancel_allow_user_paid_keep
|
|
||||||
if self.event.settings.cancel_allow_user_paid_keep_percentage:
|
|
||||||
fee += self.event.settings.cancel_allow_user_paid_keep_percentage / Decimal('100.0') * self.total
|
|
||||||
if self.event.settings.cancel_allow_user_paid_keep_fees:
|
if self.event.settings.cancel_allow_user_paid_keep_fees:
|
||||||
fee += self.fees.filter(
|
fee += self.fees.filter(
|
||||||
fee_type__in=(OrderFee.FEE_TYPE_PAYMENT, OrderFee.FEE_TYPE_SHIPPING, OrderFee.FEE_TYPE_SERVICE)
|
fee_type__in=(OrderFee.FEE_TYPE_PAYMENT, OrderFee.FEE_TYPE_SHIPPING, OrderFee.FEE_TYPE_SERVICE,
|
||||||
|
OrderFee.FEE_TYPE_CANCELLATION)
|
||||||
).aggregate(
|
).aggregate(
|
||||||
s=Sum('value')
|
s=Sum('value')
|
||||||
)['s'] or 0
|
)['s'] or 0
|
||||||
|
if self.event.settings.cancel_allow_user_paid_keep_percentage:
|
||||||
|
fee += self.event.settings.cancel_allow_user_paid_keep_percentage / Decimal('100.0') * (self.total - fee)
|
||||||
|
if self.event.settings.cancel_allow_user_paid_keep:
|
||||||
|
fee += self.event.settings.cancel_allow_user_paid_keep
|
||||||
return round_decimal(fee, self.event.currency)
|
return round_decimal(fee, self.event.currency)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -464,6 +474,8 @@ class Order(LockModel, LoggedModel):
|
|||||||
"""
|
"""
|
||||||
from .checkin import Checkin
|
from .checkin import Checkin
|
||||||
|
|
||||||
|
if self.cancellation_requests.exists():
|
||||||
|
return False
|
||||||
positions = list(
|
positions = list(
|
||||||
self.positions.all().annotate(
|
self.positions.all().annotate(
|
||||||
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk')))
|
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk')))
|
||||||
@@ -586,10 +598,11 @@ class Order(LockModel, LoggedModel):
|
|||||||
|
|
||||||
modify_deadline = self.event.settings.get('last_order_modification_date', as_type=RelativeDateWrapper)
|
modify_deadline = self.event.settings.get('last_order_modification_date', as_type=RelativeDateWrapper)
|
||||||
if self.event.has_subevents and modify_deadline:
|
if self.event.has_subevents and modify_deadline:
|
||||||
modify_deadline = min([
|
dates = [
|
||||||
modify_deadline.datetime(se)
|
modify_deadline.datetime(se)
|
||||||
for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True))
|
for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True))
|
||||||
])
|
]
|
||||||
|
modify_deadline = min(dates) if dates else None
|
||||||
elif modify_deadline:
|
elif modify_deadline:
|
||||||
modify_deadline = modify_deadline.datetime(self.event)
|
modify_deadline = modify_deadline.datetime(self.event)
|
||||||
|
|
||||||
@@ -620,10 +633,11 @@ class Order(LockModel, LoggedModel):
|
|||||||
dl_date = self.event.settings.get('ticket_download_date', as_type=RelativeDateWrapper)
|
dl_date = self.event.settings.get('ticket_download_date', as_type=RelativeDateWrapper)
|
||||||
if dl_date:
|
if dl_date:
|
||||||
if self.event.has_subevents:
|
if self.event.has_subevents:
|
||||||
dl_date = min([
|
dates = [
|
||||||
dl_date.datetime(se)
|
dl_date.datetime(se)
|
||||||
for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True))
|
for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True))
|
||||||
])
|
]
|
||||||
|
dl_date = min(dates) if dates else None
|
||||||
else:
|
else:
|
||||||
dl_date = dl_date.datetime(self.event)
|
dl_date = dl_date.datetime(self.event)
|
||||||
return dl_date
|
return dl_date
|
||||||
@@ -648,10 +662,14 @@ class Order(LockModel, LoggedModel):
|
|||||||
term_last = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
|
term_last = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
|
||||||
if term_last:
|
if term_last:
|
||||||
if self.event.has_subevents:
|
if self.event.has_subevents:
|
||||||
term_last = min([
|
terms = [
|
||||||
term_last.datetime(se).date()
|
term_last.datetime(se).date()
|
||||||
for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True))
|
for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True))
|
||||||
])
|
]
|
||||||
|
if terms:
|
||||||
|
term_last = min(terms)
|
||||||
|
else:
|
||||||
|
term_last = None
|
||||||
else:
|
else:
|
||||||
term_last = term_last.datetime(self.event).date()
|
term_last = term_last.datetime(self.event).date()
|
||||||
term_last = make_aware(datetime.combine(
|
term_last = make_aware(datetime.combine(
|
||||||
@@ -683,16 +701,19 @@ class Order(LockModel, LoggedModel):
|
|||||||
|
|
||||||
return self._is_still_available(count_waitinglist=count_waitinglist, force=force)
|
return self._is_still_available(count_waitinglist=count_waitinglist, force=force)
|
||||||
|
|
||||||
def _is_still_available(self, now_dt: datetime=None, count_waitinglist=True, force=False) -> Union[bool, str]:
|
def _is_still_available(self, now_dt: datetime=None, count_waitinglist=True, force=False,
|
||||||
|
check_voucher_usage=False) -> Union[bool, str]:
|
||||||
error_messages = {
|
error_messages = {
|
||||||
'unavailable': _('The ordered product "{item}" is no longer available.'),
|
'unavailable': _('The ordered product "{item}" is no longer available.'),
|
||||||
'seat_unavailable': _('The seat "{seat}" is no longer available.'),
|
'seat_unavailable': _('The seat "{seat}" is no longer available.'),
|
||||||
'voucher_budget': _('The voucher "{voucher}" no longer has sufficient budget.'),
|
'voucher_budget': _('The voucher "{voucher}" no longer has sufficient budget.'),
|
||||||
|
'voucher_usages': _('The voucher "{voucher}" has been used in the meantime.'),
|
||||||
}
|
}
|
||||||
now_dt = now_dt or now()
|
now_dt = now_dt or now()
|
||||||
positions = self.positions.all().select_related('item', 'variation', 'seat', 'voucher')
|
positions = self.positions.all().select_related('item', 'variation', 'seat', 'voucher')
|
||||||
quota_cache = {}
|
quota_cache = {}
|
||||||
v_budget = {}
|
v_budget = {}
|
||||||
|
v_usage = Counter()
|
||||||
try:
|
try:
|
||||||
for i, op in enumerate(positions):
|
for i, op in enumerate(positions):
|
||||||
if op.seat:
|
if op.seat:
|
||||||
@@ -711,6 +732,13 @@ class Order(LockModel, LoggedModel):
|
|||||||
))
|
))
|
||||||
v_budget[op.voucher] -= disc
|
v_budget[op.voucher] -= disc
|
||||||
|
|
||||||
|
if op.voucher and check_voucher_usage:
|
||||||
|
v_usage[op.voucher.pk] += 1
|
||||||
|
if v_usage[op.voucher.pk] + op.voucher.redeemed > op.voucher.max_usages:
|
||||||
|
raise Quota.QuotaExceededException(error_messages['voucher_usages'].format(
|
||||||
|
voucher=op.voucher.code
|
||||||
|
))
|
||||||
|
|
||||||
quotas = list(op.quotas)
|
quotas = list(op.quotas)
|
||||||
if len(quotas) == 0:
|
if len(quotas) == 0:
|
||||||
raise Quota.QuotaExceededException(error_messages['unavailable'].format(
|
raise Quota.QuotaExceededException(error_messages['unavailable'].format(
|
||||||
@@ -900,7 +928,7 @@ class QuestionAnswer(models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_image(self):
|
def is_image(self):
|
||||||
return any(self.file.name.endswith(e) for e in ('.jpg', '.png', '.gif', '.tiff', '.bmp', '.jpeg'))
|
return any(self.file.name.lower().endswith(e) for e in ('.jpg', '.png', '.gif', '.tiff', '.bmp', '.jpeg'))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def file_name(self):
|
def file_name(self):
|
||||||
@@ -1038,6 +1066,13 @@ class AbstractPosition(models.Model):
|
|||||||
'Seat', null=True, blank=True, on_delete=models.PROTECT
|
'Seat', null=True, blank=True, on_delete=models.PROTECT
|
||||||
)
|
)
|
||||||
|
|
||||||
|
company = models.CharField(max_length=255, blank=True, verbose_name=_('Company name'), null=True)
|
||||||
|
street = models.TextField(verbose_name=_('Address'), blank=True, null=True)
|
||||||
|
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=True, null=True)
|
||||||
|
city = models.CharField(max_length=255, verbose_name=_('City'), blank=True, null=True)
|
||||||
|
country = CountryField(verbose_name=_('Country'), blank=True, blank_label=_('Select country'), null=True)
|
||||||
|
state = models.CharField(max_length=255, verbose_name=pgettext_lazy('address', 'State'), blank=True, null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
@@ -1258,6 +1293,36 @@ class OrderPayment(models.Model):
|
|||||||
self.order.log_action('pretix.event.order.overpaid', {}, user=user, auth=auth)
|
self.order.log_action('pretix.event.order.overpaid', {}, user=user, auth=auth)
|
||||||
order_paid.send(self.order.event, order=self.order)
|
order_paid.send(self.order.event, order=self.order)
|
||||||
|
|
||||||
|
def fail(self, info=None, user=None, auth=None):
|
||||||
|
"""
|
||||||
|
Marks the order as failed and sets info to ``info``, but only if the order is in ``created`` or ``pending``
|
||||||
|
state. This is equivalent to setting ``state`` to ``OrderPayment.PAYMENT_STATE_FAILED`` and logging a failure,
|
||||||
|
but it adds strong database logging since we do not want to report a failure for an order that has just
|
||||||
|
been marked as paid.
|
||||||
|
"""
|
||||||
|
with transaction.atomic():
|
||||||
|
locked_instance = OrderPayment.objects.select_for_update().get(pk=self.pk)
|
||||||
|
if locked_instance.state not in (OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING):
|
||||||
|
# Race condition detected, this payment is already confirmed
|
||||||
|
logger.info('Failed payment {} but ignored due to likely race condition.'.format(
|
||||||
|
self.full_id,
|
||||||
|
))
|
||||||
|
return
|
||||||
|
|
||||||
|
if isinstance(info, str):
|
||||||
|
locked_instance.info = info
|
||||||
|
elif info:
|
||||||
|
locked_instance.info_data = info
|
||||||
|
locked_instance.state = OrderPayment.PAYMENT_STATE_FAILED
|
||||||
|
locked_instance.save(update_fields=['state', 'info'])
|
||||||
|
|
||||||
|
self.refresh_from_db()
|
||||||
|
self.order.log_action('pretix.event.order.payment.failed', {
|
||||||
|
'local_id': self.local_id,
|
||||||
|
'provider': self.provider,
|
||||||
|
'info': info,
|
||||||
|
}, user=user, auth=auth)
|
||||||
|
|
||||||
def confirm(self, count_waitinglist=True, send_mail=True, force=False, user=None, auth=None, mail_text='',
|
def confirm(self, count_waitinglist=True, send_mail=True, force=False, user=None, auth=None, mail_text='',
|
||||||
ignore_date=False, lock=True, payment_date=None):
|
ignore_date=False, lock=True, payment_date=None):
|
||||||
"""
|
"""
|
||||||
@@ -1285,6 +1350,9 @@ class OrderPayment(models.Model):
|
|||||||
locked_instance = OrderPayment.objects.select_for_update().get(pk=self.pk)
|
locked_instance = OrderPayment.objects.select_for_update().get(pk=self.pk)
|
||||||
if locked_instance.state == self.PAYMENT_STATE_CONFIRMED:
|
if locked_instance.state == self.PAYMENT_STATE_CONFIRMED:
|
||||||
# Race condition detected, this payment is already confirmed
|
# Race condition detected, this payment is already confirmed
|
||||||
|
logger.info('Confirmed payment {} but ignored due to likely race condition.'.format(
|
||||||
|
self.full_id,
|
||||||
|
))
|
||||||
return
|
return
|
||||||
|
|
||||||
locked_instance.state = self.PAYMENT_STATE_CONFIRMED
|
locked_instance.state = self.PAYMENT_STATE_CONFIRMED
|
||||||
@@ -1305,6 +1373,7 @@ class OrderPayment(models.Model):
|
|||||||
}, user=user, auth=auth)
|
}, user=user, auth=auth)
|
||||||
|
|
||||||
if self.order.status in (Order.STATUS_PAID, Order.STATUS_CANCELED):
|
if self.order.status in (Order.STATUS_PAID, Order.STATUS_CANCELED):
|
||||||
|
logger.info('Confirmed payment {} but order is in status {}.'.format(self.full_id, self.order.status))
|
||||||
return
|
return
|
||||||
|
|
||||||
payment_sum = self.order.payments.filter(
|
payment_sum = self.order.payments.filter(
|
||||||
@@ -1315,6 +1384,9 @@ class OrderPayment(models.Model):
|
|||||||
OrderRefund.REFUND_STATE_CREATED)
|
OrderRefund.REFUND_STATE_CREATED)
|
||||||
).aggregate(s=Sum('amount'))['s'] or Decimal('0.00')
|
).aggregate(s=Sum('amount'))['s'] or Decimal('0.00')
|
||||||
if payment_sum - refund_sum < self.order.total:
|
if payment_sum - refund_sum < self.order.total:
|
||||||
|
logger.info('Confirmed payment {} but payment sum is {} and refund sum is.'.format(
|
||||||
|
self.full_id, payment_sum, refund_sum
|
||||||
|
))
|
||||||
return
|
return
|
||||||
|
|
||||||
if (self.order.status == Order.STATUS_PENDING and self.order.expires > now() + timedelta(hours=12)) or not lock:
|
if (self.order.status == Order.STATUS_PENDING and self.order.expires > now() + timedelta(hours=12)) or not lock:
|
||||||
@@ -1873,8 +1945,9 @@ class OrderPosition(AbstractPosition):
|
|||||||
if self.tax_rate is None:
|
if self.tax_rate is None:
|
||||||
self._calculate_tax()
|
self._calculate_tax()
|
||||||
self.order.touch()
|
self.order.touch()
|
||||||
if self.pk is None:
|
if not self.pk:
|
||||||
while OrderPosition.all.filter(secret=self.secret).exists():
|
while OrderPosition.all.filter(secret=self.secret,
|
||||||
|
order__event__organizer_id=self.order.event.organizer_id).exists():
|
||||||
self.secret = generate_position_secret()
|
self.secret = generate_position_secret()
|
||||||
|
|
||||||
if not self.pseudonymization_id:
|
if not self.pseudonymization_id:
|
||||||
@@ -1891,9 +1964,10 @@ class OrderPosition(AbstractPosition):
|
|||||||
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
|
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
|
||||||
while True:
|
while True:
|
||||||
code = get_random_string(length=10, allowed_chars=charset)
|
code = get_random_string(length=10, allowed_chars=charset)
|
||||||
if not OrderPosition.all.filter(pseudonymization_id=code).exists():
|
with scopes_disabled():
|
||||||
self.pseudonymization_id = code
|
if not OrderPosition.all.filter(pseudonymization_id=code).exists():
|
||||||
return
|
self.pseudonymization_id = code
|
||||||
|
return
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def event(self):
|
def event(self):
|
||||||
@@ -2040,11 +2114,13 @@ class InvoiceAddress(models.Model):
|
|||||||
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=False)
|
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=False)
|
||||||
city = models.CharField(max_length=255, verbose_name=_('City'), blank=False)
|
city = models.CharField(max_length=255, verbose_name=_('City'), blank=False)
|
||||||
country_old = models.CharField(max_length=255, verbose_name=_('Country'), blank=False)
|
country_old = models.CharField(max_length=255, verbose_name=_('Country'), blank=False)
|
||||||
country = CountryField(verbose_name=_('Country'), blank=False, blank_label=_('Select country'))
|
country = CountryField(verbose_name=_('Country'), blank=False, blank_label=_('Select country'),
|
||||||
|
countries=CachedCountries)
|
||||||
state = models.CharField(max_length=255, verbose_name=pgettext_lazy('address', 'State'), blank=True)
|
state = models.CharField(max_length=255, verbose_name=pgettext_lazy('address', 'State'), blank=True)
|
||||||
vat_id = models.CharField(max_length=255, blank=True, verbose_name=_('VAT ID'),
|
vat_id = models.CharField(max_length=255, blank=True, verbose_name=_('VAT ID'),
|
||||||
help_text=_('Only for business customers within the EU.'))
|
help_text=_('Only for business customers within the EU.'))
|
||||||
vat_id_validated = models.BooleanField(default=False)
|
vat_id_validated = models.BooleanField(default=False)
|
||||||
|
custom_field = models.CharField(max_length=255, null=True, blank=True)
|
||||||
internal_reference = models.TextField(
|
internal_reference = models.TextField(
|
||||||
verbose_name=_('Internal reference'),
|
verbose_name=_('Internal reference'),
|
||||||
help_text=_('This reference will be printed on your invoice for your convenience.'),
|
help_text=_('This reference will be printed on your invoice for your convenience.'),
|
||||||
@@ -2146,6 +2222,13 @@ class CachedCombinedTicket(models.Model):
|
|||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
|
||||||
|
class CancellationRequest(models.Model):
|
||||||
|
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='cancellation_requests')
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
cancellation_fee = models.DecimalField(max_digits=10, decimal_places=2)
|
||||||
|
refund_as_giftcard = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_delete, sender=CachedTicket)
|
@receiver(post_delete, sender=CachedTicket)
|
||||||
def cachedticket_delete(sender, instance, **kwargs):
|
def cachedticket_delete(sender, instance, **kwargs):
|
||||||
if instance.file:
|
if instance.file:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from django.db import models
|
|||||||
from django.db.models import Exists, OuterRef, Q
|
from django.db.models import Exists, OuterRef, Q
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.models.base import LoggedModel
|
from pretix.base.models.base import LoggedModel
|
||||||
from pretix.base.validators import OrganizerSlugBanlistValidator
|
from pretix.base.validators import OrganizerSlugBanlistValidator
|
||||||
@@ -37,7 +37,7 @@ class Organizer(LoggedModel):
|
|||||||
"once. This is being used in URLs to refer to your organizer accounts and your events."),
|
"once. This is being used in URLs to refer to your organizer accounts and your events."),
|
||||||
validators=[
|
validators=[
|
||||||
RegexValidator(
|
RegexValidator(
|
||||||
regex="^[a-zA-Z0-9.-]+$",
|
regex="^[a-zA-Z0-9][a-zA-Z0-9.-]+$",
|
||||||
message=_("The slug may only contain letters, numbers, dots and dashes.")
|
message=_("The slug may only contain letters, numbers, dots and dashes.")
|
||||||
),
|
),
|
||||||
OrganizerSlugBanlistValidator()
|
OrganizerSlugBanlistValidator()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from django.db import models
|
|||||||
from django.db.models import F, Q
|
from django.db.models import F, Q
|
||||||
from django.utils.deconstruct import deconstructible
|
from django.utils.deconstruct import deconstructible
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext, ugettext_lazy as _
|
from django.utils.translation import gettext, gettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.models import Event, Item, LoggedModel, Organizer, SubEvent
|
from pretix.base.models import Event, Item, LoggedModel, Organizer, SubEvent
|
||||||
|
|
||||||
@@ -28,7 +28,8 @@ class SeatingPlanLayoutValidator:
|
|||||||
try:
|
try:
|
||||||
jsonschema.validate(val, schema)
|
jsonschema.validate(val, schema)
|
||||||
except jsonschema.ValidationError as e:
|
except jsonschema.ValidationError as e:
|
||||||
raise ValidationError(_('Your layout file is not a valid seating plan. Error message: {}').format(str(e)))
|
e = str(e).replace('%', '%%')
|
||||||
|
raise ValidationError(_('Your layout file is not a valid seating plan. Error message: {}').format(e))
|
||||||
|
|
||||||
|
|
||||||
class SeatingPlan(LoggedModel):
|
class SeatingPlan(LoggedModel):
|
||||||
@@ -40,7 +41,7 @@ class SeatingPlan(LoggedModel):
|
|||||||
layout = models.TextField(validators=[SeatingPlanLayoutValidator()])
|
layout = models.TextField(validators=[SeatingPlanLayoutValidator()])
|
||||||
|
|
||||||
Category = namedtuple('Categrory', 'name')
|
Category = namedtuple('Categrory', 'name')
|
||||||
RawSeat = namedtuple('Seat', 'name guid number row category zone sorting_rank')
|
RawSeat = namedtuple('Seat', 'name guid number row category zone sorting_rank row_label seat_label')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@@ -69,11 +70,17 @@ class SeatingPlan(LoggedModel):
|
|||||||
# optimization, because this way we do not need to update the rank of very seat if we change a plan a little.
|
# optimization, because this way we do not need to update the rank of very seat if we change a plan a little.
|
||||||
for zi, z in enumerate(self.layout_data['zones']):
|
for zi, z in enumerate(self.layout_data['zones']):
|
||||||
for ri, r in enumerate(z['rows']):
|
for ri, r in enumerate(z['rows']):
|
||||||
|
row_label = None
|
||||||
|
if r.get('row_label'):
|
||||||
|
row_label = r['row_label'].replace("%s", r.get('row_number', str(ri)))
|
||||||
try:
|
try:
|
||||||
row_rank = int(r['row_number'])
|
row_rank = int(r['row_number'])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
row_rank = ri
|
row_rank = ri
|
||||||
for si, s in enumerate(r['seats']):
|
for si, s in enumerate(r['seats']):
|
||||||
|
seat_label = None
|
||||||
|
if r.get('seat_label'):
|
||||||
|
seat_label = r['seat_label'].replace("%s", s.get('seat_number', str(si)))
|
||||||
try:
|
try:
|
||||||
seat_rank = int(s['seat_number'])
|
seat_rank = int(s['seat_number'])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@@ -87,6 +94,8 @@ class SeatingPlan(LoggedModel):
|
|||||||
guid=s['seat_guid'],
|
guid=s['seat_guid'],
|
||||||
name='{} {}'.format(r['row_number'], s['seat_number']), # TODO: Zone? Variable scheme?
|
name='{} {}'.format(r['row_number'], s['seat_number']), # TODO: Zone? Variable scheme?
|
||||||
row=r['row_number'],
|
row=r['row_number'],
|
||||||
|
row_label=row_label,
|
||||||
|
seat_label=seat_label,
|
||||||
zone=z['name'],
|
zone=z['name'],
|
||||||
category=s['category'],
|
category=s['category'],
|
||||||
sorting_rank=rank
|
sorting_rank=rank
|
||||||
@@ -114,7 +123,9 @@ class Seat(models.Model):
|
|||||||
name = models.CharField(max_length=190)
|
name = models.CharField(max_length=190)
|
||||||
zone_name = models.CharField(max_length=190, blank=True, default="")
|
zone_name = models.CharField(max_length=190, blank=True, default="")
|
||||||
row_name = models.CharField(max_length=190, blank=True, default="")
|
row_name = models.CharField(max_length=190, blank=True, default="")
|
||||||
|
row_label = models.CharField(max_length=190, null=True)
|
||||||
seat_number = models.CharField(max_length=190, blank=True, default="")
|
seat_number = models.CharField(max_length=190, blank=True, default="")
|
||||||
|
seat_label = models.CharField(max_length=190, null=True)
|
||||||
seat_guid = models.CharField(max_length=190, db_index=True)
|
seat_guid = models.CharField(max_length=190, db_index=True)
|
||||||
product = models.ForeignKey('Item', null=True, blank=True, related_name='seats', on_delete=models.CASCADE)
|
product = models.ForeignKey('Item', null=True, blank=True, related_name='seats', on_delete=models.CASCADE)
|
||||||
blocked = models.BooleanField(default=False)
|
blocked = models.BooleanField(default=False)
|
||||||
@@ -127,10 +138,17 @@ class Seat(models.Model):
|
|||||||
parts = []
|
parts = []
|
||||||
if self.zone_name:
|
if self.zone_name:
|
||||||
parts.append(self.zone_name)
|
parts.append(self.zone_name)
|
||||||
if self.row_name:
|
|
||||||
|
if self.row_label:
|
||||||
|
parts.append(self.row_label)
|
||||||
|
elif self.row_name:
|
||||||
parts.append(gettext('Row {number}').format(number=self.row_name))
|
parts.append(gettext('Row {number}').format(number=self.row_name))
|
||||||
if self.seat_number:
|
|
||||||
|
if self.seat_label:
|
||||||
|
parts.append(self.seat_label)
|
||||||
|
elif self.seat_number:
|
||||||
parts.append(gettext('Seat {number}').format(number=self.seat_number))
|
parts.append(gettext('Seat {number}').format(number=self.seat_number))
|
||||||
|
|
||||||
if not parts:
|
if not parts:
|
||||||
return self.name
|
return self.name
|
||||||
return ', '.join(parts)
|
return ', '.join(parts)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from decimal import Decimal
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.formats import localize
|
from django.utils.formats import localize
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_countries.fields import CountryField
|
from django_countries.fields import CountryField
|
||||||
from i18nfield.fields import I18nCharField
|
from i18nfield.fields import I18nCharField
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from django.db.models import F, OuterRef, Q, Subquery, Sum
|
|||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
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 gettext_lazy as _, pgettext_lazy
|
||||||
from django_scopes import ScopedManager, scopes_disabled
|
from django_scopes import ScopedManager, scopes_disabled
|
||||||
|
|
||||||
from pretix.base.banlist import banned
|
from pretix.base.banlist import banned
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from datetime import timedelta
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
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 gettext_lazy as _, pgettext_lazy
|
||||||
from django_scopes import ScopedManager
|
from django_scopes import ScopedManager
|
||||||
|
|
||||||
from pretix.base.email import get_email_context
|
from pretix.base.email import get_email_context
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from collections import OrderedDict, namedtuple
|
|||||||
|
|
||||||
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.translation import pgettext_lazy, ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
|
|
||||||
from pretix.base.models import Event, LogEntry
|
from pretix.base.models import Event, LogEntry
|
||||||
from pretix.base.signals import register_notification_types
|
from pretix.base.signals import register_notification_types
|
||||||
@@ -223,6 +223,12 @@ def register_default_notification_types(sender, **kwargs):
|
|||||||
_('Order canceled'),
|
_('Order canceled'),
|
||||||
_('Order {order.code} has been canceled.')
|
_('Order {order.code} has been canceled.')
|
||||||
),
|
),
|
||||||
|
ParametrizedOrderNotificationType(
|
||||||
|
sender,
|
||||||
|
'pretix.event.order.reactivated',
|
||||||
|
_('Order reactivated'),
|
||||||
|
_('Order {order.code} has been reactivated.')
|
||||||
|
),
|
||||||
ParametrizedOrderNotificationType(
|
ParametrizedOrderNotificationType(
|
||||||
sender,
|
sender,
|
||||||
'pretix.event.order.expired',
|
'pretix.event.order.expired',
|
||||||
|
|||||||
@@ -325,7 +325,7 @@ class InvoiceAddressState(ImportColumn):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def verbose_name(self):
|
def verbose_name(self):
|
||||||
return _('Invoice address') + ': ' + _('State')
|
return _('Invoice address') + ': ' + pgettext('address', 'State')
|
||||||
|
|
||||||
def clean(self, value, previous_values):
|
def clean(self, value, previous_values):
|
||||||
if value:
|
if value:
|
||||||
@@ -398,6 +398,99 @@ class AttendeeEmail(ImportColumn):
|
|||||||
position.attendee_email = value
|
position.attendee_email = value
|
||||||
|
|
||||||
|
|
||||||
|
class AttendeeCompany(ImportColumn):
|
||||||
|
identifier = 'attendee_company'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def verbose_name(self):
|
||||||
|
return _('Attendee address') + ': ' + _('Company')
|
||||||
|
|
||||||
|
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||||
|
position.company = value or ''
|
||||||
|
|
||||||
|
|
||||||
|
class AttendeeStreet(ImportColumn):
|
||||||
|
identifier = 'attendee_street'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def verbose_name(self):
|
||||||
|
return _('Attendee address') + ': ' + _('Address')
|
||||||
|
|
||||||
|
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||||
|
position.address = value or ''
|
||||||
|
|
||||||
|
|
||||||
|
class AttendeeZip(ImportColumn):
|
||||||
|
identifier = 'attendee_zipcode'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def verbose_name(self):
|
||||||
|
return _('Attendee address') + ': ' + _('ZIP code')
|
||||||
|
|
||||||
|
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||||
|
position.zipcode = value or ''
|
||||||
|
|
||||||
|
|
||||||
|
class AttendeeCity(ImportColumn):
|
||||||
|
identifier = 'attendee_city'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def verbose_name(self):
|
||||||
|
return _('Attendee address') + ': ' + _('City')
|
||||||
|
|
||||||
|
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||||
|
position.city = value or ''
|
||||||
|
|
||||||
|
|
||||||
|
class AttendeeCountry(ImportColumn):
|
||||||
|
identifier = 'attendee_country'
|
||||||
|
default_value = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def initial(self):
|
||||||
|
return 'static:' + str(guess_country(self.event))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def verbose_name(self):
|
||||||
|
return _('Attendee address') + ': ' + _('Country')
|
||||||
|
|
||||||
|
def static_choices(self):
|
||||||
|
return list(countries)
|
||||||
|
|
||||||
|
def clean(self, value, previous_values):
|
||||||
|
if value and not Country(value).numeric:
|
||||||
|
raise ValidationError(_("Please enter a valid country code."))
|
||||||
|
return value
|
||||||
|
|
||||||
|
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||||
|
position.country = value or ''
|
||||||
|
|
||||||
|
|
||||||
|
class AttendeeState(ImportColumn):
|
||||||
|
identifier = 'attendee_state'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def verbose_name(self):
|
||||||
|
return _('Attendee address') + ': ' + _('State')
|
||||||
|
|
||||||
|
def clean(self, value, previous_values):
|
||||||
|
if value:
|
||||||
|
if previous_values.get('attendee_country') not in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||||
|
raise ValidationError(_("States are not supported for this country."))
|
||||||
|
|
||||||
|
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[previous_values.get('attendee_country')]
|
||||||
|
match = [
|
||||||
|
s for s in pycountry.subdivisions.get(country_code=previous_values.get('attendee_country'))
|
||||||
|
if s.type in types and (s.code[3:] == value or s.name == value)
|
||||||
|
]
|
||||||
|
if len(match) == 0:
|
||||||
|
raise ValidationError(_("Please enter a valid state."))
|
||||||
|
return match[0].code[3:]
|
||||||
|
|
||||||
|
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||||
|
position.state = value or ''
|
||||||
|
|
||||||
|
|
||||||
class Price(ImportColumn):
|
class Price(ImportColumn):
|
||||||
identifier = 'price'
|
identifier = 'price'
|
||||||
verbose_name = gettext_lazy('Price')
|
verbose_name = gettext_lazy('Price')
|
||||||
@@ -596,6 +689,12 @@ def get_all_columns(event):
|
|||||||
default.append(AttendeeNamePart(event, n, l))
|
default.append(AttendeeNamePart(event, n, l))
|
||||||
default += [
|
default += [
|
||||||
AttendeeEmail(event),
|
AttendeeEmail(event),
|
||||||
|
AttendeeCompany(event),
|
||||||
|
AttendeeStreet(event),
|
||||||
|
AttendeeZip(event),
|
||||||
|
AttendeeCity(event),
|
||||||
|
AttendeeCountry(event),
|
||||||
|
AttendeeState(event),
|
||||||
Price(event),
|
Price(event),
|
||||||
Secret(event),
|
Secret(event),
|
||||||
Locale(event),
|
Locale(event),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
@@ -14,8 +15,9 @@ 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
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
|
from django.utils.crypto import get_random_string
|
||||||
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 gettext_lazy as _, pgettext_lazy
|
||||||
from django_countries import Countries
|
from django_countries import Countries
|
||||||
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
|
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
|
||||||
from i18nfield.strings import LazyI18nString
|
from i18nfield.strings import LazyI18nString
|
||||||
@@ -27,12 +29,13 @@ from pretix.base.models import (
|
|||||||
OrderRefund, Quota,
|
OrderRefund, Quota,
|
||||||
)
|
)
|
||||||
from pretix.base.reldate import RelativeDateField, RelativeDateWrapper
|
from pretix.base.reldate import RelativeDateField, RelativeDateWrapper
|
||||||
|
from pretix.base.services.cart import get_fees
|
||||||
from pretix.base.settings import SettingsSandbox
|
from pretix.base.settings import SettingsSandbox
|
||||||
from pretix.base.signals import register_payment_providers
|
from pretix.base.signals import register_payment_providers
|
||||||
from pretix.base.templatetags.money import money_filter
|
from pretix.base.templatetags.money import money_filter
|
||||||
from pretix.base.templatetags.rich_text import rich_text
|
from pretix.base.templatetags.rich_text import rich_text
|
||||||
from pretix.helpers.money import DecimalTextInput
|
from pretix.helpers.money import DecimalTextInput
|
||||||
from pretix.multidomain.urlreverse import eventreverse
|
from pretix.multidomain.urlreverse import build_absolute_uri, eventreverse
|
||||||
from pretix.presale.views import get_cart, get_cart_total
|
from pretix.presale.views import get_cart, get_cart_total
|
||||||
from pretix.presale.views.cart import cart_session, get_or_create_cart_id
|
from pretix.presale.views.cart import cart_session, get_or_create_cart_id
|
||||||
|
|
||||||
@@ -204,6 +207,13 @@ class BasePaymentProvider:
|
|||||||
implementation.
|
implementation.
|
||||||
"""
|
"""
|
||||||
places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
|
places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
|
||||||
|
|
||||||
|
if not self.settings.get('_hidden_seed'):
|
||||||
|
self.settings.set('_hidden_seed', get_random_string(64))
|
||||||
|
hidden_url = build_absolute_uri(self.event, 'presale:event.payment.unlock', kwargs={
|
||||||
|
'hash': hashlib.sha256((self.settings._hidden_seed + self.event.slug).encode()).hexdigest(),
|
||||||
|
})
|
||||||
|
|
||||||
d = OrderedDict([
|
d = OrderedDict([
|
||||||
('_enabled',
|
('_enabled',
|
||||||
forms.BooleanField(
|
forms.BooleanField(
|
||||||
@@ -297,7 +307,30 @@ class BasePaymentProvider:
|
|||||||
widget=forms.CheckboxSelectMultiple,
|
widget=forms.CheckboxSelectMultiple,
|
||||||
help_text=_(
|
help_text=_(
|
||||||
'Only allow the usage of this payment provider in the following sales channels'),
|
'Only allow the usage of this payment provider in the following sales channels'),
|
||||||
))
|
)),
|
||||||
|
('_hidden',
|
||||||
|
forms.BooleanField(
|
||||||
|
label=_('Hide payment method'),
|
||||||
|
required=False,
|
||||||
|
help_text=_(
|
||||||
|
'The payment method will not be shown by default but only to people who enter the shop through '
|
||||||
|
'a special link.'
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
('_hidden_url',
|
||||||
|
forms.URLField(
|
||||||
|
label=_('Link to enable payment method'),
|
||||||
|
widget=forms.TextInput(attrs={
|
||||||
|
'readonly': 'readonly',
|
||||||
|
'data-display-dependency': '#id_%s_hidden' % self.settings.get_prefix(),
|
||||||
|
'value': hidden_url,
|
||||||
|
}),
|
||||||
|
required=False,
|
||||||
|
initial=hidden_url,
|
||||||
|
help_text=_(
|
||||||
|
'Share this link with customers who should use this payment method.'
|
||||||
|
),
|
||||||
|
)),
|
||||||
])
|
])
|
||||||
d['_restricted_countries']._as_type = list
|
d['_restricted_countries']._as_type = list
|
||||||
d['_restrict_to_sales_channels']._as_type = list
|
d['_restrict_to_sales_channels']._as_type = list
|
||||||
@@ -378,28 +411,31 @@ class BasePaymentProvider:
|
|||||||
availability_date = self.settings.get('_availability_date', as_type=RelativeDateWrapper)
|
availability_date = self.settings.get('_availability_date', as_type=RelativeDateWrapper)
|
||||||
if availability_date:
|
if availability_date:
|
||||||
if self.event.has_subevents and cart_id:
|
if self.event.has_subevents and cart_id:
|
||||||
availability_date = min([
|
dates = [
|
||||||
availability_date.datetime(se).date()
|
availability_date.datetime(se).date()
|
||||||
for se in self.event.subevents.filter(
|
for se in self.event.subevents.filter(
|
||||||
id__in=CartPosition.objects.filter(
|
id__in=CartPosition.objects.filter(
|
||||||
cart_id=cart_id, event=self.event
|
cart_id=cart_id, event=self.event
|
||||||
).values_list('subevent', flat=True)
|
).values_list('subevent', flat=True)
|
||||||
)
|
)
|
||||||
])
|
]
|
||||||
|
availability_date = min(dates) if dates else None
|
||||||
elif self.event.has_subevents and order:
|
elif self.event.has_subevents and order:
|
||||||
availability_date = min([
|
dates = [
|
||||||
availability_date.datetime(se).date()
|
availability_date.datetime(se).date()
|
||||||
for se in self.event.subevents.filter(
|
for se in self.event.subevents.filter(
|
||||||
id__in=order.positions.values_list('subevent', flat=True)
|
id__in=order.positions.values_list('subevent', flat=True)
|
||||||
)
|
)
|
||||||
])
|
]
|
||||||
|
availability_date = min(dates) if dates else None
|
||||||
elif self.event.has_subevents:
|
elif self.event.has_subevents:
|
||||||
logger.error('Payment provider is not subevent-ready.')
|
logger.error('Payment provider is not subevent-ready.')
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
availability_date = availability_date.datetime(self.event).date()
|
availability_date = availability_date.datetime(self.event).date()
|
||||||
|
|
||||||
return availability_date >= now_dt.astimezone(tz).date()
|
if availability_date:
|
||||||
|
return availability_date >= now_dt.astimezone(tz).date()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -433,6 +469,11 @@ class BasePaymentProvider:
|
|||||||
if self.settings._total_min is not None:
|
if self.settings._total_min is not None:
|
||||||
pricing = pricing and total >= Decimal(self.settings._total_min)
|
pricing = pricing and total >= Decimal(self.settings._total_min)
|
||||||
|
|
||||||
|
if self.settings.get('_hidden', as_type=bool):
|
||||||
|
hashes = request.session.get('pretix_unlock_hashes', [])
|
||||||
|
if hashlib.sha256((self.settings._hidden_seed + self.event.slug).encode()).hexdigest() not in hashes:
|
||||||
|
return False
|
||||||
|
|
||||||
def get_invoice_address():
|
def get_invoice_address():
|
||||||
if not hasattr(request, '_checkout_flow_invoice_address'):
|
if not hasattr(request, '_checkout_flow_invoice_address'):
|
||||||
cs = cart_session(request)
|
cs = cart_session(request)
|
||||||
@@ -602,6 +643,9 @@ class BasePaymentProvider:
|
|||||||
if self.settings._total_min is not None and ps < Decimal(self.settings._total_min):
|
if self.settings._total_min is not None and ps < Decimal(self.settings._total_min):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if self.settings.get('_hidden', as_type=bool):
|
||||||
|
return False
|
||||||
|
|
||||||
restricted_countries = self.settings.get('_restricted_countries', as_type=list)
|
restricted_countries = self.settings.get('_restricted_countries', as_type=list)
|
||||||
if restricted_countries:
|
if restricted_countries:
|
||||||
try:
|
try:
|
||||||
@@ -687,7 +731,7 @@ class BasePaymentProvider:
|
|||||||
On failure, you should raise a PaymentException.
|
On failure, you should raise a PaymentException.
|
||||||
"""
|
"""
|
||||||
payment.state = OrderPayment.PAYMENT_STATE_CANCELED
|
payment.state = OrderPayment.PAYMENT_STATE_CANCELED
|
||||||
payment.save()
|
payment.save(update_fields=['state'])
|
||||||
|
|
||||||
def execute_refund(self, refund: OrderRefund):
|
def execute_refund(self, refund: OrderRefund):
|
||||||
"""
|
"""
|
||||||
@@ -721,6 +765,16 @@ class BasePaymentProvider:
|
|||||||
"""
|
"""
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
def matching_id(self, payment: OrderPayment):
|
||||||
|
"""
|
||||||
|
Will be called to get an ID for a matching this payment when comparing pretix records with records of an external
|
||||||
|
source. This should return the main transaction ID for your API.
|
||||||
|
|
||||||
|
:param payment: The payment in question.
|
||||||
|
:return: A string or None
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class PaymentException(Exception):
|
class PaymentException(Exception):
|
||||||
pass
|
pass
|
||||||
@@ -1053,8 +1107,16 @@ class GiftCardPayment(BasePaymentProvider):
|
|||||||
return
|
return
|
||||||
cs['gift_cards'] = cs['gift_cards'] + [gc.pk]
|
cs['gift_cards'] = cs['gift_cards'] + [gc.pk]
|
||||||
|
|
||||||
remainder = cart['total'] - gc.value
|
total = sum(p.total for p in cart['positions'])
|
||||||
if remainder >= Decimal('0.00'):
|
# Recompute fees. Some plugins, e.g. pretix-servicefees, change their fee schedule if a gift card is
|
||||||
|
# applied.
|
||||||
|
fees = get_fees(
|
||||||
|
self.event, request, total, cart['invoice_address'], cs.get('payment'),
|
||||||
|
cart['raw']
|
||||||
|
)
|
||||||
|
total += sum([f.value for f in fees])
|
||||||
|
remainder = total - gc.value
|
||||||
|
if remainder > Decimal('0.00'):
|
||||||
del cs['payment']
|
del cs['payment']
|
||||||
messages.success(request, _("Your gift card has been applied, but {} still need to be paid. Please select a payment method.").format(
|
messages.success(request, _("Your gift card has been applied, but {} still need to be paid. Please select a payment method.").format(
|
||||||
money_filter(remainder, self.event.currency)
|
money_filter(remainder, self.event.currency)
|
||||||
@@ -1157,6 +1219,7 @@ class GiftCardPayment(BasePaymentProvider):
|
|||||||
)
|
)
|
||||||
refund.info_data = {
|
refund.info_data = {
|
||||||
'gift_card': gc.pk,
|
'gift_card': gc.pk,
|
||||||
|
'gift_card_code': gc.secret,
|
||||||
'transaction_id': trans.pk,
|
'transaction_id': trans.pk,
|
||||||
}
|
}
|
||||||
refund.done()
|
refund.done()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import copy
|
import copy
|
||||||
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
import uuid
|
import uuid
|
||||||
@@ -9,16 +9,15 @@ from collections import OrderedDict
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
import bleach
|
|
||||||
from arabic_reshaper import ArabicReshaper
|
from arabic_reshaper import ArabicReshaper
|
||||||
from bidi.algorithm import get_display
|
from bidi.algorithm import get_display
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.staticfiles import finders
|
from django.contrib.staticfiles import finders
|
||||||
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.html import escape
|
from django.utils.html import conditional_escape
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from PyPDF2 import PdfFileReader
|
from PyPDF2 import PdfFileReader
|
||||||
from pytz import timezone
|
from pytz import timezone
|
||||||
from reportlab.graphics import renderPDF
|
from reportlab.graphics import renderPDF
|
||||||
@@ -64,32 +63,32 @@ DEFAULT_VARIABLES = OrderedDict((
|
|||||||
("item", {
|
("item", {
|
||||||
"label": _("Product name"),
|
"label": _("Product name"),
|
||||||
"editor_sample": _("Sample product"),
|
"editor_sample": _("Sample product"),
|
||||||
"evaluate": lambda orderposition, order, event: escape(str(orderposition.item.name))
|
"evaluate": lambda orderposition, order, event: str(orderposition.item.name)
|
||||||
}),
|
}),
|
||||||
("variation", {
|
("variation", {
|
||||||
"label": _("Variation name"),
|
"label": _("Variation name"),
|
||||||
"editor_sample": _("Sample variation"),
|
"editor_sample": _("Sample variation"),
|
||||||
"evaluate": lambda op, order, event: escape(str(op.variation) if op.variation else '')
|
"evaluate": lambda op, order, event: str(op.variation) if op.variation else ''
|
||||||
}),
|
}),
|
||||||
("item_description", {
|
("item_description", {
|
||||||
"label": _("Product description"),
|
"label": _("Product description"),
|
||||||
"editor_sample": _("Sample product description"),
|
"editor_sample": _("Sample product description"),
|
||||||
"evaluate": lambda orderposition, order, event: escape(str(orderposition.item.description))
|
"evaluate": lambda orderposition, order, event: str(orderposition.item.description)
|
||||||
}),
|
}),
|
||||||
("itemvar", {
|
("itemvar", {
|
||||||
"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: escape((
|
"evaluate": lambda orderposition, order, event: (
|
||||||
'{} - {}'.format(orderposition.item.name, orderposition.variation)
|
'{} - {}'.format(orderposition.item.name, orderposition.variation)
|
||||||
if orderposition.variation else str(orderposition.item.name)
|
if orderposition.variation else str(orderposition.item.name)
|
||||||
))
|
)
|
||||||
}),
|
}),
|
||||||
("item_category", {
|
("item_category", {
|
||||||
"label": _("Product category"),
|
"label": _("Product category"),
|
||||||
"editor_sample": _("Ticket category"),
|
"editor_sample": _("Ticket category"),
|
||||||
"evaluate": lambda orderposition, order, event: escape((
|
"evaluate": lambda orderposition, order, event: (
|
||||||
str(orderposition.item.category.name) if orderposition.item.category else ""
|
str(orderposition.item.category.name) if orderposition.item.category else ""
|
||||||
))
|
)
|
||||||
}),
|
}),
|
||||||
("price", {
|
("price", {
|
||||||
"label": _("Price"),
|
"label": _("Price"),
|
||||||
@@ -108,12 +107,12 @@ DEFAULT_VARIABLES = OrderedDict((
|
|||||||
("attendee_name", {
|
("attendee_name", {
|
||||||
"label": _("Attendee name"),
|
"label": _("Attendee name"),
|
||||||
"editor_sample": _("John Doe"),
|
"editor_sample": _("John Doe"),
|
||||||
"evaluate": lambda op, order, ev: escape(op.attendee_name or (op.addon_to.attendee_name if op.addon_to else ''))
|
"evaluate": lambda op, order, ev: op.attendee_name or (op.addon_to.attendee_name if op.addon_to else '')
|
||||||
}),
|
}),
|
||||||
("event_name", {
|
("event_name", {
|
||||||
"label": _("Event name"),
|
"label": _("Event name"),
|
||||||
"editor_sample": _("Sample event name"),
|
"editor_sample": _("Sample event name"),
|
||||||
"evaluate": lambda op, order, ev: escape(str(ev.name))
|
"evaluate": lambda op, order, ev: str(ev.name)
|
||||||
}),
|
}),
|
||||||
("event_date", {
|
("event_date", {
|
||||||
"label": _("Event date"),
|
"label": _("Event date"),
|
||||||
@@ -189,27 +188,32 @@ DEFAULT_VARIABLES = OrderedDict((
|
|||||||
("event_location", {
|
("event_location", {
|
||||||
"label": _("Event location"),
|
"label": _("Event location"),
|
||||||
"editor_sample": _("Random City"),
|
"editor_sample": _("Random City"),
|
||||||
"evaluate": lambda op, order, ev: str(ev.location).replace("\n", "<br/>\n")
|
"evaluate": lambda op, order, ev: str(ev.location)
|
||||||
}),
|
}),
|
||||||
("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: escape(order.invoice_address.name if getattr(order, 'invoice_address', None) else '')
|
"evaluate": lambda op, order, ev: order.invoice_address.name if getattr(order, 'invoice_address', None) else ''
|
||||||
}),
|
}),
|
||||||
("invoice_company", {
|
("invoice_company", {
|
||||||
"label": _("Invoice address company"),
|
"label": _("Invoice address company"),
|
||||||
"editor_sample": _("Sample company"),
|
"editor_sample": _("Sample company"),
|
||||||
"evaluate": lambda op, order, ev: escape(order.invoice_address.company if getattr(order, 'invoice_address', None) else '')
|
"evaluate": lambda op, order, ev: order.invoice_address.company if getattr(order, 'invoice_address', None) else ''
|
||||||
}),
|
}),
|
||||||
("invoice_city", {
|
("invoice_city", {
|
||||||
"label": _("Invoice address city"),
|
"label": _("Invoice address city"),
|
||||||
"editor_sample": _("Sample city"),
|
"editor_sample": _("Sample city"),
|
||||||
"evaluate": lambda op, order, ev: escape(order.invoice_address.city if getattr(order, 'invoice_address', None) else '')
|
"evaluate": lambda op, order, ev: order.invoice_address.city if getattr(order, 'invoice_address', None) else ''
|
||||||
|
}),
|
||||||
|
("attendee_company", {
|
||||||
|
"label": _("Attendee company"),
|
||||||
|
"editor_sample": _("Sample company"),
|
||||||
|
"evaluate": lambda op, order, ev: op.company or (op.addon_to.company if op.addon_to else '')
|
||||||
}),
|
}),
|
||||||
("addons", {
|
("addons", {
|
||||||
"label": _("List of Add-Ons"),
|
"label": _("List of Add-Ons"),
|
||||||
"editor_sample": _("Addon 1\nAddon 2"),
|
"editor_sample": _("Addon 1\nAddon 2"),
|
||||||
"evaluate": lambda op, order, ev: "<br/>".join([
|
"evaluate": lambda op, order, ev: "\n".join([
|
||||||
'{} - {}'.format(p.item, p.variation) if p.variation else str(p.item)
|
'{} - {}'.format(p.item, p.variation) if p.variation else str(p.item)
|
||||||
for p in (
|
for p in (
|
||||||
op.addons.all() if 'addons' in getattr(op, '_prefetched_objects_cache', {})
|
op.addons.all() if 'addons' in getattr(op, '_prefetched_objects_cache', {})
|
||||||
@@ -221,7 +225,7 @@ DEFAULT_VARIABLES = OrderedDict((
|
|||||||
("organizer", {
|
("organizer", {
|
||||||
"label": _("Organizer name"),
|
"label": _("Organizer name"),
|
||||||
"editor_sample": _("Event organizer company"),
|
"editor_sample": _("Event organizer company"),
|
||||||
"evaluate": lambda op, order, ev: escape(str(order.event.organizer.name))
|
"evaluate": lambda op, order, ev: str(order.event.organizer.name)
|
||||||
}),
|
}),
|
||||||
("organizer_info_text", {
|
("organizer_info_text", {
|
||||||
"label": _("Organizer info text"),
|
"label": _("Organizer info text"),
|
||||||
@@ -301,7 +305,7 @@ def variables_from_questions(sender, *args, **kwargs):
|
|||||||
if not a:
|
if not a:
|
||||||
return ""
|
return ""
|
||||||
else:
|
else:
|
||||||
return escape(str(a)).replace("\n", "<br/>\n")
|
return str(a)
|
||||||
|
|
||||||
d = {}
|
d = {}
|
||||||
for q in sender.questions.all():
|
for q in sender.questions.all():
|
||||||
@@ -314,11 +318,13 @@ def variables_from_questions(sender, *args, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
def _get_attendee_name_part(key, op, order, ev):
|
def _get_attendee_name_part(key, op, order, ev):
|
||||||
return escape(op.attendee_name_parts.get(key, ''))
|
if isinstance(key, tuple):
|
||||||
|
return ' '.join(p for p in [_get_attendee_name_part(c[0], op, order, ev) for c in key] if p)
|
||||||
|
return op.attendee_name_parts.get(key, '')
|
||||||
|
|
||||||
|
|
||||||
def _get_ia_name_part(key, op, order, ev):
|
def _get_ia_name_part(key, op, order, ev):
|
||||||
return escape(order.invoice_address.name_parts.get(key, '') if getattr(order, 'invoice_address', None) else '')
|
return order.invoice_address.name_parts.get(key, '') if getattr(order, 'invoice_address', None) else ''
|
||||||
|
|
||||||
|
|
||||||
def get_variables(event):
|
def get_variables(event):
|
||||||
@@ -331,6 +337,13 @@ def get_variables(event):
|
|||||||
'editor_sample': scheme['sample'][key],
|
'editor_sample': scheme['sample'][key],
|
||||||
'evaluate': partial(_get_attendee_name_part, key)
|
'evaluate': partial(_get_attendee_name_part, key)
|
||||||
}
|
}
|
||||||
|
for i in range(2, len(scheme['fields']) + 1):
|
||||||
|
for comb in itertools.combinations(scheme['fields'], i):
|
||||||
|
v['attendee_name_%s' % ('_'.join(c[0] for c in comb))] = {
|
||||||
|
'label': _("Attendee name: {part}").format(part=' + '.join(str(c[1]) for c in comb)),
|
||||||
|
'editor_sample': ' '.join(str(scheme['sample'][c[0]]) for c in comb),
|
||||||
|
'evaluate': partial(_get_attendee_name_part, comb)
|
||||||
|
}
|
||||||
|
|
||||||
v['invoice_name']['editor_sample'] = scheme['concatenation'](scheme['sample'])
|
v['invoice_name']['editor_sample'] = scheme['concatenation'](scheme['sample'])
|
||||||
v['attendee_name']['editor_sample'] = scheme['concatenation'](scheme['sample'])
|
v['attendee_name']['editor_sample'] = scheme['concatenation'](scheme['sample'])
|
||||||
@@ -422,7 +435,9 @@ class Renderer:
|
|||||||
if not o['content']:
|
if not o['content']:
|
||||||
return '(error)'
|
return '(error)'
|
||||||
if o['content'] == 'other':
|
if o['content'] == 'other':
|
||||||
return o['text'].replace("\n", "<br/>\n")
|
return o['text']
|
||||||
|
elif o['content'].startswith('itemmeta:'):
|
||||||
|
return op.item.meta_data.get(o['content'][9:]) or ''
|
||||||
elif o['content'].startswith('meta:'):
|
elif o['content'].startswith('meta:'):
|
||||||
return ev.meta_data.get(o['content'][5:]) or ''
|
return ev.meta_data.get(o['content'][5:]) or ''
|
||||||
elif o['content'] in self.variables:
|
elif o['content'] in self.variables:
|
||||||
@@ -454,13 +469,9 @@ class Renderer:
|
|||||||
textColor=Color(o['color'][0] / 255, o['color'][1] / 255, o['color'][2] / 255),
|
textColor=Color(o['color'][0] / 255, o['color'][1] / 255, o['color'][2] / 255),
|
||||||
alignment=align_map[o['align']]
|
alignment=align_map[o['align']]
|
||||||
)
|
)
|
||||||
text = re.sub(
|
text = conditional_escape(
|
||||||
"<br[^>]*>", "<br/>",
|
self._get_text_content(op, order, o) or "",
|
||||||
bleach.clean(
|
).replace("\n", "<br/>\n")
|
||||||
self._get_text_content(op, order, o) or "",
|
|
||||||
tags=["br"], attributes={}, styles=[], strip=True
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
|
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
|
||||||
# to resolve all ligatures and python-bidi to switch RTL texts.
|
# to resolve all ligatures and python-bidi to switch RTL texts.
|
||||||
@@ -488,7 +499,7 @@ class Renderer:
|
|||||||
p.drawOn(canvas, 0, -h - ad[1])
|
p.drawOn(canvas, 0, -h - ad[1])
|
||||||
canvas.restoreState()
|
canvas.restoreState()
|
||||||
|
|
||||||
def draw_page(self, canvas: Canvas, order: Order, op: OrderPosition):
|
def draw_page(self, canvas: Canvas, order: Order, op: OrderPosition, show_page=True):
|
||||||
for o in self.layout:
|
for o in self.layout:
|
||||||
if o['type'] == "barcodearea":
|
if o['type'] == "barcodearea":
|
||||||
self._draw_barcodearea(canvas, op, o)
|
self._draw_barcodearea(canvas, op, o)
|
||||||
@@ -498,7 +509,8 @@ class Renderer:
|
|||||||
self._draw_poweredby(canvas, op, o)
|
self._draw_poweredby(canvas, op, o)
|
||||||
if self.bg_pdf:
|
if self.bg_pdf:
|
||||||
canvas.setPageSize((self.bg_pdf.getPage(0).mediaBox[2], self.bg_pdf.getPage(0).mediaBox[3]))
|
canvas.setPageSize((self.bg_pdf.getPage(0).mediaBox[2], self.bg_pdf.getPage(0).mediaBox[3]))
|
||||||
canvas.showPage()
|
if show_page:
|
||||||
|
canvas.showPage()
|
||||||
|
|
||||||
def render_background(self, buffer, title=_('Ticket')):
|
def render_background(self, buffer, title=_('Ticket')):
|
||||||
if settings.PDFTK:
|
if settings.PDFTK:
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ from dateutil import parser
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
BASE_CHOICES = (
|
BASE_CHOICES = (
|
||||||
('date_from', _('Event start')),
|
('date_from', _('Event start')),
|
||||||
@@ -115,6 +116,8 @@ class RelativeDateWrapper:
|
|||||||
base_date_name=parts[3],
|
base_date_name=parts[3],
|
||||||
time=time
|
time=time
|
||||||
)
|
)
|
||||||
|
if data.base_date_name not in [k[0] for k in BASE_CHOICES]:
|
||||||
|
raise ValueError('{} is not a valid base date'.format(data.base_date_name))
|
||||||
else:
|
else:
|
||||||
data = parser.parse(input)
|
data = parser.parse(input)
|
||||||
return RelativeDateWrapper(data)
|
return RelativeDateWrapper(data)
|
||||||
@@ -321,7 +324,7 @@ class ModelRelativeDateTimeField(models.CharField):
|
|||||||
return value.to_string()
|
return value.to_string()
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def from_db_value(self, value, expression, connection, context):
|
def from_db_value(self, value, expression, connection):
|
||||||
if value is None:
|
if value is None:
|
||||||
return None
|
return None
|
||||||
return RelativeDateWrapper.from_string(value)
|
return RelativeDateWrapper.from_string(value)
|
||||||
@@ -330,3 +333,39 @@ class ModelRelativeDateTimeField(models.CharField):
|
|||||||
defaults = {'form_class': self.form_class}
|
defaults = {'form_class': self.form_class}
|
||||||
defaults.update(kwargs)
|
defaults.update(kwargs)
|
||||||
return super().formfield(**defaults)
|
return super().formfield(**defaults)
|
||||||
|
|
||||||
|
|
||||||
|
class SerializerRelativeDateField(serializers.CharField):
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
if data is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
r = RelativeDateWrapper.from_string(data)
|
||||||
|
if isinstance(r.data, RelativeDate):
|
||||||
|
if r.data.time is not None:
|
||||||
|
raise ValidationError("Do not specify a time for a date field")
|
||||||
|
return r
|
||||||
|
except:
|
||||||
|
raise ValidationError("Invalid relative date")
|
||||||
|
|
||||||
|
def to_representation(self, value: RelativeDateWrapper):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
return value.to_string()
|
||||||
|
|
||||||
|
|
||||||
|
class SerializerRelativeDateTimeField(serializers.CharField):
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
if data is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return RelativeDateWrapper.from_string(data)
|
||||||
|
except:
|
||||||
|
raise ValidationError("Invalid relative date")
|
||||||
|
|
||||||
|
def to_representation(self, value: RelativeDateWrapper):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
return value.to_string()
|
||||||
|
|||||||
224
src/pretix/base/services/cancelevent.py
Normal file
224
src/pretix/base/services/cancelevent.py
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
import logging
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
|
from django.db.models import Count, Exists, IntegerField, OuterRef, Subquery
|
||||||
|
from i18nfield.strings import LazyI18nString
|
||||||
|
|
||||||
|
from pretix.base.decimal import round_decimal
|
||||||
|
from pretix.base.email import get_email_context
|
||||||
|
from pretix.base.i18n import language
|
||||||
|
from pretix.base.models import (
|
||||||
|
Event, InvoiceAddress, Order, OrderFee, OrderPosition, OrderRefund,
|
||||||
|
SubEvent, User, WaitingListEntry,
|
||||||
|
)
|
||||||
|
from pretix.base.services.locking import LockTimeoutException
|
||||||
|
from pretix.base.services.mail import SendMailException, TolerantDict, mail
|
||||||
|
from pretix.base.services.orders import (
|
||||||
|
OrderChangeManager, OrderError, _cancel_order, _try_auto_refund,
|
||||||
|
)
|
||||||
|
from pretix.base.services.tasks import ProfiledEventTask
|
||||||
|
from pretix.celery_app import app
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _send_wle_mail(wle: WaitingListEntry, subject: LazyI18nString, message: LazyI18nString, subevent: SubEvent):
|
||||||
|
with language(wle.locale):
|
||||||
|
email_context = get_email_context(event_or_subevent=subevent or wle.event, event=wle.event)
|
||||||
|
try:
|
||||||
|
mail(
|
||||||
|
wle.email,
|
||||||
|
str(subject).format_map(TolerantDict(email_context)),
|
||||||
|
message,
|
||||||
|
email_context,
|
||||||
|
wle.event,
|
||||||
|
locale=wle.locale
|
||||||
|
)
|
||||||
|
except SendMailException:
|
||||||
|
logger.exception('Waiting list canceled email could not be sent')
|
||||||
|
|
||||||
|
|
||||||
|
def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, subevent: SubEvent,
|
||||||
|
refund_amount: Decimal, user: User, positions: list):
|
||||||
|
with language(order.locale):
|
||||||
|
try:
|
||||||
|
ia = order.invoice_address
|
||||||
|
except InvoiceAddress.DoesNotExist:
|
||||||
|
ia = InvoiceAddress()
|
||||||
|
|
||||||
|
email_context = get_email_context(event_or_subevent=subevent or order.event, refund_amount=refund_amount,
|
||||||
|
order=order, position_or_address=ia, event=order.event)
|
||||||
|
real_subject = str(subject).format_map(TolerantDict(email_context))
|
||||||
|
try:
|
||||||
|
order.send_mail(
|
||||||
|
real_subject, message, email_context,
|
||||||
|
'pretix.event.order.email.event_canceled',
|
||||||
|
user,
|
||||||
|
)
|
||||||
|
except SendMailException:
|
||||||
|
logger.exception('Order canceled email could not be sent')
|
||||||
|
|
||||||
|
for p in positions:
|
||||||
|
if subevent and p.subevent_id != subevent.id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
|
||||||
|
real_subject = str(subject).format_map(TolerantDict(email_context))
|
||||||
|
email_context = get_email_context(event_or_subevent=subevent or order.event,
|
||||||
|
event=order.event,
|
||||||
|
refund_amount=refund_amount,
|
||||||
|
position_or_address=p,
|
||||||
|
order=order, position=p)
|
||||||
|
try:
|
||||||
|
order.send_mail(
|
||||||
|
real_subject, message, email_context,
|
||||||
|
'pretix.event.order.email.event_canceled',
|
||||||
|
position=p,
|
||||||
|
user=user
|
||||||
|
)
|
||||||
|
except SendMailException:
|
||||||
|
logger.exception('Order canceled email could not be sent to attendee')
|
||||||
|
|
||||||
|
|
||||||
|
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
|
||||||
|
def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_fixed: str,
|
||||||
|
keep_fee_percentage: str, keep_fees: list=None, manual_refund: bool=False,
|
||||||
|
send: bool=False, send_subject: dict=None, send_message: dict=None,
|
||||||
|
send_waitinglist: bool=False, send_waitinglist_subject: dict={}, send_waitinglist_message: dict={},
|
||||||
|
user: int=None, refund_as_giftcard: bool=False):
|
||||||
|
send_subject = LazyI18nString(send_subject)
|
||||||
|
send_message = LazyI18nString(send_message)
|
||||||
|
send_waitinglist_subject = LazyI18nString(send_waitinglist_subject)
|
||||||
|
send_waitinglist_message = LazyI18nString(send_waitinglist_message)
|
||||||
|
if user:
|
||||||
|
user = User.objects.get(pk=user)
|
||||||
|
|
||||||
|
s = OrderPosition.objects.filter(
|
||||||
|
order=OuterRef('pk')
|
||||||
|
).order_by().values('order').annotate(k=Count('id')).values('k')
|
||||||
|
orders_to_cancel = event.orders.annotate(pcnt=Subquery(s, output_field=IntegerField())).filter(
|
||||||
|
status__in=[Order.STATUS_PAID, Order.STATUS_PENDING, Order.STATUS_EXPIRED],
|
||||||
|
pcnt__gt=0
|
||||||
|
).all()
|
||||||
|
|
||||||
|
if subevent:
|
||||||
|
subevent = event.subevents.get(pk=subevent)
|
||||||
|
|
||||||
|
has_subevent = OrderPosition.objects.filter(order_id=OuterRef('pk')).filter(
|
||||||
|
subevent=subevent
|
||||||
|
)
|
||||||
|
has_other_subevent = OrderPosition.objects.filter(order_id=OuterRef('pk')).exclude(
|
||||||
|
subevent=subevent
|
||||||
|
)
|
||||||
|
orders_to_change = orders_to_cancel.annotate(
|
||||||
|
has_subevent=Exists(has_subevent),
|
||||||
|
has_other_subevent=Exists(has_other_subevent),
|
||||||
|
).filter(
|
||||||
|
has_subevent=True, has_other_subevent=True
|
||||||
|
)
|
||||||
|
orders_to_cancel = orders_to_cancel.annotate(
|
||||||
|
has_subevent=Exists(has_subevent),
|
||||||
|
has_other_subevent=Exists(has_other_subevent),
|
||||||
|
).filter(
|
||||||
|
has_subevent=True, has_other_subevent=False
|
||||||
|
)
|
||||||
|
|
||||||
|
subevent.log_action(
|
||||||
|
'pretix.subevent.canceled', user=user,
|
||||||
|
)
|
||||||
|
subevent.active = False
|
||||||
|
subevent.save(update_fields=['active'])
|
||||||
|
subevent.log_action(
|
||||||
|
'pretix.subevent.changed', user=user, data={'active': False, '_source': 'cancel_event'}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
orders_to_change = event.orders.none()
|
||||||
|
event.log_action(
|
||||||
|
'pretix.event.canceled', user=user,
|
||||||
|
)
|
||||||
|
|
||||||
|
for i in event.items.filter(active=True):
|
||||||
|
i.active = False
|
||||||
|
i.save(update_fields=['active'])
|
||||||
|
i.log_action(
|
||||||
|
'pretix.event.item.changed', user=user, data={'active': False, '_source': 'cancel_event'}
|
||||||
|
)
|
||||||
|
failed = 0
|
||||||
|
|
||||||
|
for o in orders_to_cancel.only('id', 'total'):
|
||||||
|
try:
|
||||||
|
fee = Decimal('0.00')
|
||||||
|
fee_sum = Decimal('0.00')
|
||||||
|
keep_fee_objects = []
|
||||||
|
if keep_fees:
|
||||||
|
for f in o.fees.all():
|
||||||
|
if f.fee_type in keep_fees:
|
||||||
|
fee += f.value
|
||||||
|
keep_fee_objects.append(f)
|
||||||
|
fee_sum += f.value
|
||||||
|
if keep_fee_percentage:
|
||||||
|
fee += Decimal(keep_fee_percentage) / Decimal('100.00') * (o.total - fee_sum)
|
||||||
|
if keep_fee_fixed:
|
||||||
|
fee += Decimal(keep_fee_fixed)
|
||||||
|
fee = round_decimal(min(fee, o.payment_refund_sum), event.currency)
|
||||||
|
|
||||||
|
_cancel_order(o.pk, user, send_mail=False, cancellation_fee=fee, keep_fees=keep_fee_objects)
|
||||||
|
refund_amount = o.payment_refund_sum
|
||||||
|
|
||||||
|
try:
|
||||||
|
if auto_refund:
|
||||||
|
_try_auto_refund(o.pk, manual_refund=manual_refund, allow_partial=True,
|
||||||
|
source=OrderRefund.REFUND_SOURCE_ADMIN, refund_as_giftcard=refund_as_giftcard)
|
||||||
|
finally:
|
||||||
|
if send:
|
||||||
|
_send_mail(o, send_subject, send_message, subevent, refund_amount, user, o.positions.all())
|
||||||
|
except LockTimeoutException:
|
||||||
|
logger.exception("Could not cancel order")
|
||||||
|
failed += 1
|
||||||
|
except OrderError:
|
||||||
|
logger.exception("Could not cancel order")
|
||||||
|
failed += 1
|
||||||
|
|
||||||
|
for o in orders_to_change.values_list('id', flat=True):
|
||||||
|
with transaction.atomic():
|
||||||
|
o = event.orders.select_for_update().get(pk=o)
|
||||||
|
total = Decimal('0.00')
|
||||||
|
positions = []
|
||||||
|
|
||||||
|
ocm = OrderChangeManager(o, user=user, notify=False)
|
||||||
|
for p in o.positions.all():
|
||||||
|
if p.subevent == subevent:
|
||||||
|
total += p.price
|
||||||
|
ocm.cancel(p)
|
||||||
|
positions.append(p)
|
||||||
|
|
||||||
|
fee = Decimal('0.00')
|
||||||
|
if keep_fee_fixed:
|
||||||
|
fee += Decimal(keep_fee_fixed)
|
||||||
|
if keep_fee_percentage:
|
||||||
|
fee += Decimal(keep_fee_percentage) / Decimal('100.00') * total
|
||||||
|
fee = round_decimal(min(fee, o.payment_refund_sum), event.currency)
|
||||||
|
if fee:
|
||||||
|
f = OrderFee(
|
||||||
|
fee_type=OrderFee.FEE_TYPE_CANCELLATION,
|
||||||
|
value=fee,
|
||||||
|
order=o,
|
||||||
|
tax_rule=o.event.settings.tax_rate_default,
|
||||||
|
)
|
||||||
|
f._calculate_tax()
|
||||||
|
ocm.add_fee(f)
|
||||||
|
|
||||||
|
ocm.commit()
|
||||||
|
refund_amount = o.payment_refund_sum - o.total
|
||||||
|
|
||||||
|
if auto_refund:
|
||||||
|
_try_auto_refund(o.pk, manual_refund=manual_refund, allow_partial=True, source=OrderRefund.REFUND_SOURCE_ADMIN)
|
||||||
|
|
||||||
|
if send:
|
||||||
|
_send_mail(o, send_subject, send_message, subevent, refund_amount, user, positions)
|
||||||
|
|
||||||
|
for wle in event.waitinglistentries.filter(subevent=subevent, voucher__isnull=True):
|
||||||
|
_send_wle_mail(wle, send_waitinglist_subject, send_waitinglist_message, subevent)
|
||||||
|
|
||||||
|
return failed
|
||||||
@@ -9,7 +9,7 @@ from django.db import DatabaseError, transaction
|
|||||||
from django.db.models import Count, Exists, OuterRef, Q
|
from django.db.models import Count, Exists, OuterRef, Q
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.timezone import make_aware, now
|
from django.utils.timezone import make_aware, now
|
||||||
from django.utils.translation import pgettext_lazy, ugettext as _
|
from django.utils.translation import gettext as _, pgettext_lazy
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from pretix.base.channels import get_all_sales_channels
|
from pretix.base.channels import get_all_sales_channels
|
||||||
@@ -213,7 +213,7 @@ class CartManager:
|
|||||||
has_variations=Count('variations'),
|
has_variations=Count('variations'),
|
||||||
).filter(
|
).filter(
|
||||||
id__in=[i for i in item_ids if i and i not in self._items_cache]
|
id__in=[i for i in item_ids if i and i not in self._items_cache]
|
||||||
)
|
).order_by()
|
||||||
})
|
})
|
||||||
self._variations_cache.update({
|
self._variations_cache.update({
|
||||||
v.pk: v
|
v.pk: v
|
||||||
@@ -221,7 +221,7 @@ class CartManager:
|
|||||||
'quotas'
|
'quotas'
|
||||||
).select_related('item', 'item__event').filter(
|
).select_related('item', 'item__event').filter(
|
||||||
id__in=[i for i in variation_ids if i and i not in self._variations_cache]
|
id__in=[i for i in variation_ids if i and i not in self._variations_cache]
|
||||||
)
|
).order_by()
|
||||||
})
|
})
|
||||||
|
|
||||||
def _check_max_cart_size(self):
|
def _check_max_cart_size(self):
|
||||||
@@ -303,32 +303,6 @@ class CartManager:
|
|||||||
if op.item.require_bundling and not op.addon_to == 'FAKE':
|
if op.item.require_bundling and not op.addon_to == 'FAKE':
|
||||||
raise CartError(error_messages['bundled_only'])
|
raise CartError(error_messages['bundled_only'])
|
||||||
|
|
||||||
if op.item.max_per_order or op.item.min_per_order:
|
|
||||||
new_total = (
|
|
||||||
len([1 for p in self.positions if p.item_id == op.item.pk]) +
|
|
||||||
sum([_op.count for _op in self._operations + current_ops
|
|
||||||
if isinstance(_op, self.AddOperation) and _op.item == op.item]) +
|
|
||||||
op.count -
|
|
||||||
len([1 for _op in self._operations + current_ops
|
|
||||||
if isinstance(_op, self.RemoveOperation) and _op.position.item_id == op.item.pk])
|
|
||||||
)
|
|
||||||
|
|
||||||
if op.item.max_per_order and new_total > op.item.max_per_order:
|
|
||||||
raise CartError(
|
|
||||||
_(error_messages['max_items_per_product']) % {
|
|
||||||
'max': op.item.max_per_order,
|
|
||||||
'product': op.item.name
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if op.item.min_per_order and new_total < op.item.min_per_order:
|
|
||||||
raise CartError(
|
|
||||||
_(error_messages['min_items_per_product']) % {
|
|
||||||
'min': op.item.min_per_order,
|
|
||||||
'product': op.item.name
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_price(self, item: Item, variation: Optional[ItemVariation],
|
def _get_price(self, item: Item, variation: Optional[ItemVariation],
|
||||||
voucher: Optional[Voucher], custom_price: Optional[Decimal],
|
voucher: Optional[Voucher], custom_price: Optional[Decimal],
|
||||||
subevent: Optional[SubEvent], cp_is_net: bool=None, force_custom_price=False,
|
subevent: Optional[SubEvent], cp_is_net: bool=None, force_custom_price=False,
|
||||||
@@ -787,37 +761,48 @@ class CartManager:
|
|||||||
|
|
||||||
return vouchers_ok
|
return vouchers_ok
|
||||||
|
|
||||||
def _check_min_per_product(self):
|
def _check_min_max_per_product(self):
|
||||||
per_product = Counter()
|
items = Counter()
|
||||||
min_per_product = {}
|
|
||||||
for p in self.positions:
|
for p in self.positions:
|
||||||
per_product[p.item_id] += 1
|
items[p.item] += 1
|
||||||
min_per_product[p.item.pk] = p.item.min_per_order
|
|
||||||
|
|
||||||
for op in self._operations:
|
for op in self._operations:
|
||||||
if isinstance(op, self.AddOperation):
|
if isinstance(op, self.AddOperation):
|
||||||
per_product[op.item.pk] += op.count
|
items[op.item] += op.count
|
||||||
min_per_product[op.item.pk] = op.item.min_per_order
|
|
||||||
elif isinstance(op, self.RemoveOperation):
|
elif isinstance(op, self.RemoveOperation):
|
||||||
per_product[op.position.item_id] -= 1
|
items[op.position.item] -= 1
|
||||||
min_per_product[op.position.item.pk] = op.position.item.min_per_order
|
|
||||||
|
|
||||||
err = None
|
err = None
|
||||||
for itemid, num in per_product.items():
|
for item, count in items.items():
|
||||||
min_p = min_per_product[itemid]
|
if count == 0:
|
||||||
if min_p and num < min_p:
|
continue
|
||||||
|
|
||||||
|
if item.max_per_order and count > item.max_per_order:
|
||||||
|
raise CartError(
|
||||||
|
_(error_messages['max_items_per_product']) % {
|
||||||
|
'max': item.max_per_order,
|
||||||
|
'product': item.name
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if item.min_per_order and count < item.min_per_order:
|
||||||
self._operations = [o for o in self._operations if not (
|
self._operations = [o for o in self._operations if not (
|
||||||
isinstance(o, self.AddOperation) and o.item.pk == itemid
|
isinstance(o, self.AddOperation) and o.item.pk == item.pk
|
||||||
)]
|
)]
|
||||||
removals = [o.position.pk for o in self._operations if isinstance(o, self.RemoveOperation)]
|
removals = [o.position.pk for o in self._operations if isinstance(o, self.RemoveOperation)]
|
||||||
for p in self.positions:
|
for p in self.positions:
|
||||||
if p.item_id == itemid and p.pk not in removals:
|
if p.item_id == item.pk and p.pk not in removals:
|
||||||
self._operations.append(self.RemoveOperation(position=p))
|
self._operations.append(self.RemoveOperation(position=p))
|
||||||
err = _(error_messages['min_items_per_product_removed']) % {
|
err = _(error_messages['min_items_per_product_removed']) % {
|
||||||
'min': min_p,
|
'min': item.min_per_order,
|
||||||
'product': p.item.name
|
'product': item.name
|
||||||
}
|
}
|
||||||
|
if not err:
|
||||||
|
raise CartError(
|
||||||
|
_(error_messages['min_items_per_product']) % {
|
||||||
|
'min': item.min_per_order,
|
||||||
|
'product': item.name
|
||||||
|
}
|
||||||
|
)
|
||||||
return err
|
return err
|
||||||
|
|
||||||
def _perform_operations(self):
|
def _perform_operations(self):
|
||||||
@@ -826,7 +811,7 @@ class CartManager:
|
|||||||
err = None
|
err = None
|
||||||
new_cart_positions = []
|
new_cart_positions = []
|
||||||
|
|
||||||
err = err or self._check_min_per_product()
|
err = err or self._check_min_max_per_product()
|
||||||
|
|
||||||
self._operations.sort(key=lambda a: self.order[type(a)])
|
self._operations.sort(key=lambda a: self.order[type(a)])
|
||||||
seats_seen = set()
|
seats_seen = set()
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ from django.db import transaction
|
|||||||
from django.db.models import Prefetch
|
from django.db.models import Prefetch
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Checkin, CheckinList, Order, OrderPosition, Question, QuestionOption,
|
Checkin, CheckinList, Order, OrderPosition, Question, QuestionOption,
|
||||||
)
|
)
|
||||||
from pretix.base.signals import order_placed
|
from pretix.base.signals import checkin_created, order_placed
|
||||||
|
|
||||||
|
|
||||||
class CheckInError(Exception):
|
class CheckInError(Exception):
|
||||||
@@ -143,6 +143,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
|||||||
'datetime': dt,
|
'datetime': dt,
|
||||||
'list': clist.pk
|
'list': clist.pk
|
||||||
}, user=user, auth=auth)
|
}, user=user, auth=auth)
|
||||||
|
checkin_created.send(op.order.event, checkin=ci)
|
||||||
else:
|
else:
|
||||||
if not force:
|
if not force:
|
||||||
raise CheckInError(
|
raise CheckInError(
|
||||||
@@ -171,4 +172,5 @@ def order_placed(sender, **kwargs):
|
|||||||
for op in order.positions.all():
|
for op in order.positions.all():
|
||||||
for cl in cls:
|
for cl in cls:
|
||||||
if cl.all_products or op.item_id in {i.pk for i in cl.limit_products.all()}:
|
if cl.all_products or op.item_id in {i.pk for i in cl.limit_products.all()}:
|
||||||
Checkin.objects.create(position=op, list=cl, auto_checked_in=True)
|
ci = Checkin.objects.create(position=op, list=cl, auto_checked_in=True)
|
||||||
|
checkin_created.send(event, checkin=ci)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from typing import Any, Dict
|
|||||||
|
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.utils.timezone import override
|
from django.utils.timezone import override
|
||||||
from django.utils.translation import ugettext
|
from django.utils.translation import gettext
|
||||||
|
|
||||||
from pretix.base.i18n import LazyLocaleException, language
|
from pretix.base.i18n import LazyLocaleException, language
|
||||||
from pretix.base.models import CachedFile, Event, cachedfile_name
|
from pretix.base.models import CachedFile, Event, cachedfile_name
|
||||||
@@ -26,7 +26,7 @@ def export(event: Event, fileid: str, provider: str, form_data: Dict[str, Any])
|
|||||||
d = ex.render(form_data)
|
d = ex.render(form_data)
|
||||||
if d is None:
|
if d is None:
|
||||||
raise ExportError(
|
raise ExportError(
|
||||||
ugettext('Your export did not contain any data.')
|
gettext('Your export did not contain any data.')
|
||||||
)
|
)
|
||||||
file.filename, file.type, data = d
|
file.filename, file.type, data = d
|
||||||
file.file.save(cachedfile_name(file, file.filename), ContentFile(data))
|
file.file.save(cachedfile_name(file, file.filename), ContentFile(data))
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from django.dispatch import receiver
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import pgettext, ugettext as _
|
from django.utils.translation import gettext as _, pgettext
|
||||||
from django_countries.fields import Country
|
from django_countries.fields import Country
|
||||||
from django_scopes import scope, scopes_disabled
|
from django_scopes import scope, scopes_disabled
|
||||||
from i18nfield.strings import LazyI18nString
|
from i18nfield.strings import LazyI18nString
|
||||||
@@ -37,6 +37,10 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def build_invoice(invoice: Invoice) -> Invoice:
|
def build_invoice(invoice: Invoice) -> Invoice:
|
||||||
|
invoice.locale = invoice.event.settings.get('invoice_language', invoice.event.settings.locale)
|
||||||
|
if invoice.locale == '__user__':
|
||||||
|
invoice.locale = invoice.order.locale or invoice.event.settings.locale
|
||||||
|
|
||||||
lp = invoice.order.payments.last()
|
lp = invoice.order.payments.last()
|
||||||
|
|
||||||
with language(invoice.locale):
|
with language(invoice.locale):
|
||||||
@@ -85,6 +89,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
|||||||
).split("\n") if a.strip()
|
).split("\n") if a.strip()
|
||||||
)
|
)
|
||||||
invoice.internal_reference = ia.internal_reference
|
invoice.internal_reference = ia.internal_reference
|
||||||
|
invoice.custom_field = ia.custom_field
|
||||||
invoice.invoice_to_company = ia.company
|
invoice.invoice_to_company = ia.company
|
||||||
invoice.invoice_to_name = ia.name
|
invoice.invoice_to_name = ia.name
|
||||||
invoice.invoice_to_street = ia.street
|
invoice.invoice_to_street = ia.street
|
||||||
@@ -249,17 +254,11 @@ def regenerate_invoice(invoice: Invoice):
|
|||||||
|
|
||||||
|
|
||||||
def generate_invoice(order: Order, trigger_pdf=True):
|
def generate_invoice(order: Order, trigger_pdf=True):
|
||||||
locale = order.event.settings.get('invoice_language', order.event.settings.locale)
|
|
||||||
if locale:
|
|
||||||
if locale == '__user__':
|
|
||||||
locale = order.locale or order.event.settings.locale
|
|
||||||
|
|
||||||
invoice = Invoice(
|
invoice = Invoice(
|
||||||
order=order,
|
order=order,
|
||||||
event=order.event,
|
event=order.event,
|
||||||
organizer=order.event.organizer,
|
organizer=order.event.organizer,
|
||||||
date=timezone.now().date(),
|
date=timezone.now().date(),
|
||||||
locale=locale
|
|
||||||
)
|
)
|
||||||
invoice = build_invoice(invoice)
|
invoice = build_invoice(invoice)
|
||||||
if trigger_pdf:
|
if trigger_pdf:
|
||||||
@@ -313,7 +312,7 @@ def build_preview_invoice_pdf(event):
|
|||||||
|
|
||||||
with rolledback_transaction(), language(locale):
|
with rolledback_transaction(), language(locale):
|
||||||
order = event.orders.create(status=Order.STATUS_PENDING, datetime=timezone.now(),
|
order = event.orders.create(status=Order.STATUS_PENDING, datetime=timezone.now(),
|
||||||
expires=timezone.now(), code="PREVIEW", total=119)
|
expires=timezone.now(), code="PREVIEW", total=100 * event.tax_rules.count())
|
||||||
invoice = Invoice(
|
invoice = Invoice(
|
||||||
order=order, event=event, invoice_no="PREVIEW",
|
order=order, event=event, invoice_no="PREVIEW",
|
||||||
date=timezone.now().date(), locale=locale, organizer=event.organizer
|
date=timezone.now().date(), locale=locale, organizer=event.organizer
|
||||||
@@ -351,7 +350,7 @@ def build_preview_invoice_pdf(event):
|
|||||||
|
|
||||||
if event.tax_rules.exists():
|
if event.tax_rules.exists():
|
||||||
for i, tr in enumerate(event.tax_rules.all()):
|
for i, tr in enumerate(event.tax_rules.all()):
|
||||||
tax = tr.tax(Decimal('100.00'))
|
tax = tr.tax(Decimal('100.00'), base_price_is='gross')
|
||||||
InvoiceLine.objects.create(
|
InvoiceLine.objects.create(
|
||||||
invoice=invoice, description=_("Sample product {}").format(i + 1),
|
invoice=invoice, description=_("Sample product {}").format(i + 1),
|
||||||
gross_value=tax.gross, tax_value=tax.tax,
|
gross_value=tax.gross, tax_value=tax.tax,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from django.core.mail import (
|
|||||||
)
|
)
|
||||||
from django.core.mail.message import SafeMIMEText
|
from django.core.mail.message import SafeMIMEText
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
from django.utils.translation import pgettext, ugettext as _
|
from django.utils.translation import gettext as _, pgettext
|
||||||
from django_scopes import scope, scopes_disabled
|
from django_scopes import scope, scopes_disabled
|
||||||
from i18nfield.strings import LazyI18nString
|
from i18nfield.strings import LazyI18nString
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
|
|||||||
renderer = ClassicMailRenderer(None)
|
renderer = ClassicMailRenderer(None)
|
||||||
content_plain = body_plain = render_mail(template, context)
|
content_plain = body_plain = render_mail(template, context)
|
||||||
subject = str(subject).format_map(TolerantDict(context))
|
subject = str(subject).format_map(TolerantDict(context))
|
||||||
sender = sender or (event.settings.get('mail_from') if event else settings.MAIL_FROM)
|
sender = sender or (event.settings.get('mail_from') if event else settings.MAIL_FROM) or settings.MAIL_FROM
|
||||||
if event:
|
if event:
|
||||||
sender_name = event.settings.mail_from_name or str(event.name)
|
sender_name = event.settings.mail_from_name or str(event.name)
|
||||||
sender = formataddr((sender_name, sender))
|
sender = formataddr((sender_name, sender))
|
||||||
@@ -276,20 +276,6 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
|
|||||||
cm = lambda: scopes_disabled() # noqa
|
cm = lambda: scopes_disabled() # noqa
|
||||||
|
|
||||||
with cm():
|
with cm():
|
||||||
if invoices:
|
|
||||||
invoices = Invoice.objects.filter(pk__in=invoices)
|
|
||||||
for inv in invoices:
|
|
||||||
if inv.file:
|
|
||||||
try:
|
|
||||||
with language(inv.order.locale):
|
|
||||||
email.attach(
|
|
||||||
pgettext('invoice', 'Invoice {num}').format(num=inv.number).replace(' ', '_') + '.pdf',
|
|
||||||
inv.file.file.read(),
|
|
||||||
'application/pdf'
|
|
||||||
)
|
|
||||||
except:
|
|
||||||
logger.exception('Could not attach invoice to email')
|
|
||||||
pass
|
|
||||||
if event:
|
if event:
|
||||||
if order:
|
if order:
|
||||||
try:
|
try:
|
||||||
@@ -344,6 +330,21 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
|
|||||||
|
|
||||||
email = email_filter.send_chained(event, 'message', message=email, order=order, user=user)
|
email = email_filter.send_chained(event, 'message', message=email, order=order, user=user)
|
||||||
|
|
||||||
|
if invoices:
|
||||||
|
invoices = Invoice.objects.filter(pk__in=invoices)
|
||||||
|
for inv in invoices:
|
||||||
|
if inv.file:
|
||||||
|
try:
|
||||||
|
with language(inv.order.locale):
|
||||||
|
email.attach(
|
||||||
|
pgettext('invoice', 'Invoice {num}').format(num=inv.number).replace(' ', '_') + '.pdf',
|
||||||
|
inv.file.file.read(),
|
||||||
|
'application/pdf'
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
logger.exception('Could not attach invoice to email')
|
||||||
|
pass
|
||||||
|
|
||||||
email = global_email_filter.send_chained(event, 'message', message=email, user=user, order=order)
|
email = global_email_filter.send_chained(event, 'message', message=email, user=user, order=order)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
|
from django.utils.timezone import override
|
||||||
from django_scopes import scope, scopes_disabled
|
from django_scopes import scope, scopes_disabled
|
||||||
from inlinestyler.utils import inline_css
|
from inlinestyler.utils import inline_css
|
||||||
|
|
||||||
@@ -79,7 +80,7 @@ def send_notification(logentry_id: int, action_type: str, user_id: int, method:
|
|||||||
if not notification_type:
|
if not notification_type:
|
||||||
return # Ignore, e.g. plugin not active for this event
|
return # Ignore, e.g. plugin not active for this event
|
||||||
|
|
||||||
with language(user.locale):
|
with language(user.locale), override(logentry.event.timezone if logentry.event else user.timezone):
|
||||||
notification = notification_type.build_notification(logentry)
|
notification = notification_type.build_notification(logentry)
|
||||||
|
|
||||||
if method == "mail":
|
if method == "mail":
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user