mirror of
https://github.com/pretix/pretix.git
synced 2025-12-06 21:42:49 +00:00
Compare commits
736 Commits
pdf-fix-ro
...
back-to-th
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0630b67546 | ||
|
|
9101238743 | ||
|
|
cefbfc1ad1 | ||
|
|
69a798046e | ||
|
|
c7983bf811 | ||
|
|
042be3603b | ||
|
|
1de2320cc5 | ||
|
|
8042d9d3f4 | ||
|
|
44afe9e193 | ||
|
|
46dce1bf43 | ||
|
|
4865879978 | ||
|
|
4c793076b7 | ||
|
|
5294d819a9 | ||
|
|
7bfe94139a | ||
|
|
e5cbaa9246 | ||
|
|
37208286b1 | ||
|
|
6da56291d4 | ||
|
|
2d335cd095 | ||
|
|
569f1719b0 | ||
|
|
00dd1a5b31 | ||
|
|
18bebb6d31 | ||
|
|
77c8e81cd7 | ||
|
|
3d03f30119 | ||
|
|
91b2d685da | ||
|
|
9787ed1820 | ||
|
|
204b8e53de | ||
|
|
64358be4ae | ||
|
|
5b1175ff05 | ||
|
|
e6f56bfdc2 | ||
|
|
9610e9c89f | ||
|
|
c5f4eeeb28 | ||
|
|
b61880fb5b | ||
|
|
b29c7fc11d | ||
|
|
d99bf7437a | ||
|
|
648cc14ae0 | ||
|
|
5d71cb500a | ||
|
|
68d81982ba | ||
|
|
efa0d5f362 | ||
|
|
046898678b | ||
|
|
f38ecd0ec7 | ||
|
|
3dca6c232e | ||
|
|
297bf566ad | ||
|
|
67f09b5ede | ||
|
|
752137ad84 | ||
|
|
100528ad0f | ||
|
|
0b05eb34f4 | ||
|
|
be48c5f94c | ||
|
|
cebb6d3b43 | ||
|
|
0de96ed066 | ||
|
|
a9d506b1fa | ||
|
|
7a01057429 | ||
|
|
64e1a602d6 | ||
|
|
fe060c387a | ||
|
|
1dba4c7cc9 | ||
|
|
20b2a3d2aa | ||
|
|
044f0c5480 | ||
|
|
4d394f9e8a | ||
|
|
247c4c6c9c | ||
|
|
11a038feb3 | ||
|
|
9d57ea8534 | ||
|
|
189c77207f | ||
|
|
3422003a9c | ||
|
|
8da38ba99d | ||
|
|
fc05208b92 | ||
|
|
b163109c56 | ||
|
|
3ba818336e | ||
|
|
8aecf4f98f | ||
|
|
42f3ca9661 | ||
|
|
f7b405b210 | ||
|
|
11a6390cfc | ||
|
|
239a7746df | ||
|
|
03701eaa82 | ||
|
|
356f215d8e | ||
|
|
7962c4e380 | ||
|
|
a59711ed32 | ||
|
|
49370a5e08 | ||
|
|
980aec7326 | ||
|
|
44294110fe | ||
|
|
ce1078a783 | ||
|
|
0e0cede0ee | ||
|
|
5c833cd493 | ||
|
|
64d6a34039 | ||
|
|
cf380069b4 | ||
|
|
48168a4c68 | ||
|
|
6482fe79b0 | ||
|
|
0f696f42f6 | ||
|
|
79d59553d7 | ||
|
|
cc903c39f0 | ||
|
|
b6a42ac8d2 | ||
|
|
5f5001edb5 | ||
|
|
fb403dad88 | ||
|
|
a73c8f580d | ||
|
|
f490c89e98 | ||
|
|
159658ae46 | ||
|
|
595aff0579 | ||
|
|
b2842ec3a0 | ||
|
|
f09f07ec7c | ||
|
|
cff073f0d6 | ||
|
|
eb9d0c6cf9 | ||
|
|
e263946c3f | ||
|
|
7ee957cff0 | ||
|
|
ac2fe4b62d | ||
|
|
577e276df3 | ||
|
|
11956a8f4d | ||
|
|
d0c58713c4 | ||
|
|
46da0bda61 | ||
|
|
009e3a6d36 | ||
|
|
c01855270b | ||
|
|
2df1585b71 | ||
|
|
bf48ae567f | ||
|
|
5a72c72d18 | ||
|
|
ac02f3b417 | ||
|
|
58add74b3a | ||
|
|
0067c3537d | ||
|
|
64ae1d08a6 | ||
|
|
ca25c3c81e | ||
|
|
abbe9ec897 | ||
|
|
a7735d5d9e | ||
|
|
174c81a22b | ||
|
|
38c6294ede | ||
|
|
217ae90642 | ||
|
|
0c998ca884 | ||
|
|
b1691f867d | ||
|
|
72e451b27b | ||
|
|
8124ced6c1 | ||
|
|
a3139944f6 | ||
|
|
48493c517b | ||
|
|
535a29bf4b | ||
|
|
9d415f5179 | ||
|
|
990e9da21d | ||
|
|
4afb7a4976 | ||
|
|
22e5579ed1 | ||
|
|
79cd84e243 | ||
|
|
fad4b8846c | ||
|
|
0f4790afd8 | ||
|
|
2068a5ac29 | ||
|
|
440c97061c | ||
|
|
3b6d0c4341 | ||
|
|
06ac4b0250 | ||
|
|
a233b92f6f | ||
|
|
4ea4189e6d | ||
|
|
50838b9cea | ||
|
|
c68ee56d51 | ||
|
|
5c0587c30e | ||
|
|
f3f42a8a42 | ||
|
|
20d0a9a0ed | ||
|
|
cda8144ff0 | ||
|
|
43e8875c1e | ||
|
|
28c142b2ed | ||
|
|
46203fd8ba | ||
|
|
52e45c37df | ||
|
|
d1580dca2c | ||
|
|
cd9e672871 | ||
|
|
427f508627 | ||
|
|
887d06a485 | ||
|
|
fb49046ac1 | ||
|
|
ce826e50f7 | ||
|
|
d866c6954d | ||
|
|
40c76dda74 | ||
|
|
f532853021 | ||
|
|
8cb187502d | ||
|
|
156037f2cd | ||
|
|
134d63fb3f | ||
|
|
816002fda0 | ||
|
|
3939bbc11c | ||
|
|
95d1603cc7 | ||
|
|
ada3ada699 | ||
|
|
97eaeac4f2 | ||
|
|
d67f5c650c | ||
|
|
273c1ae0a6 | ||
|
|
a946c10ab4 | ||
|
|
2d8fba7d7c | ||
|
|
e4e0bd7ca0 | ||
|
|
3651c88289 | ||
|
|
c92ca40026 | ||
|
|
4d00efb549 | ||
|
|
7e60d13910 | ||
|
|
35800e21c7 | ||
|
|
99b4c5bd36 | ||
|
|
f121205dd1 | ||
|
|
1ac54cd209 | ||
|
|
4694719a53 | ||
|
|
9513b6e8d7 | ||
|
|
4fd7d406a0 | ||
|
|
47cb5b207a | ||
|
|
7d2cf68727 | ||
|
|
459cb47ca8 | ||
|
|
39705556cd | ||
|
|
9f794290dc | ||
|
|
b6221ab6d9 | ||
|
|
483518bce9 | ||
|
|
d9019ae735 | ||
|
|
721fd3b998 | ||
|
|
ad0d3f5469 | ||
|
|
40b44f9272 | ||
|
|
304d290f22 | ||
|
|
7592a8a575 | ||
|
|
4f33159f93 | ||
|
|
819ce6abf7 | ||
|
|
760dfd22b8 | ||
|
|
f9eaa193c9 | ||
|
|
c7720a2553 | ||
|
|
7754f5420c | ||
|
|
2c7ada6e86 | ||
|
|
3f31843fd1 | ||
|
|
952b9bd9b9 | ||
|
|
3619a6bcd0 | ||
|
|
3e2c12cdb0 | ||
|
|
a3ce3b9af3 | ||
|
|
b6461e9303 | ||
|
|
f7dfd51c2c | ||
|
|
3b98d87a26 | ||
|
|
f045062055 | ||
|
|
eb501dd1ea | ||
|
|
2d8793c355 | ||
|
|
d32bd717b7 | ||
|
|
6e6b75d55e | ||
|
|
50b5f760bb | ||
|
|
9ab2e61c31 | ||
|
|
4876a0b61f | ||
|
|
56bbcb65c3 | ||
|
|
5bb1cb498f | ||
|
|
6bf23b0fdd | ||
|
|
5deb1a8c69 | ||
|
|
1523137300 | ||
|
|
04ef097eb1 | ||
|
|
a5d4434a64 | ||
|
|
3b3c668153 | ||
|
|
d339d67111 | ||
|
|
a8aee6c824 | ||
|
|
6466987493 | ||
|
|
ff962805cd | ||
|
|
2b5f46164f | ||
|
|
d74451ded1 | ||
|
|
62f0c82d8d | ||
|
|
5b587774bb | ||
|
|
88ea8ee2ea | ||
|
|
56e0ab8378 | ||
|
|
a9ae237b1a | ||
|
|
27823b7bf6 | ||
|
|
4231cd2576 | ||
|
|
6aa5196f18 | ||
|
|
c1eac5e91e | ||
|
|
410e06364a | ||
|
|
ae137f8f16 | ||
|
|
f9f3f9f868 | ||
|
|
395eadde47 | ||
|
|
2be790fa45 | ||
|
|
f9d78eaf1a | ||
|
|
2d5d27e950 | ||
|
|
c6fa19d771 | ||
|
|
3129253eef | ||
|
|
b69ab86458 | ||
|
|
80f7ae0b76 | ||
|
|
160f9a4363 | ||
|
|
24b5b9373d | ||
|
|
178c40aee6 | ||
|
|
49c41878d2 | ||
|
|
fa4c29cf23 | ||
|
|
75b93eebc5 | ||
|
|
5a406abdd6 | ||
|
|
6712baf534 | ||
|
|
4d9243151f | ||
|
|
b89a4f7b32 | ||
|
|
c80d5b1bb2 | ||
|
|
0334c2f433 | ||
|
|
6bc46b7aec | ||
|
|
3ebe622189 | ||
|
|
25fb1ee3be | ||
|
|
a3586a73f1 | ||
|
|
93eb041acc | ||
|
|
63894ca3da | ||
|
|
73b2cce435 | ||
|
|
0a711f4965 | ||
|
|
75bf200aac | ||
|
|
11307de30a | ||
|
|
863db60786 | ||
|
|
f5a1adedca | ||
|
|
ea74688633 | ||
|
|
57738f19bf | ||
|
|
7b5ce5e198 | ||
|
|
d5f9beef69 | ||
|
|
eee39b1300 | ||
|
|
c2fdea020d | ||
|
|
f87e089734 | ||
|
|
0fad7472c0 | ||
|
|
bd0a223066 | ||
|
|
782c1a5d39 | ||
|
|
5c99d3bf69 | ||
|
|
5e6307acc9 | ||
|
|
d4bfa9d773 | ||
|
|
b7f540251c | ||
|
|
cb17d80b63 | ||
|
|
86b28b9b53 | ||
|
|
fac404631c | ||
|
|
fd547014a9 | ||
|
|
d23a625415 | ||
|
|
199416b904 | ||
|
|
0a7a113b4e | ||
|
|
5978a715b5 | ||
|
|
71beb54eb4 | ||
|
|
b92981353f | ||
|
|
a5f7115e19 | ||
|
|
f5e3d4b0bc | ||
|
|
6fba080b8f | ||
|
|
365ccf159e | ||
|
|
b40a41d742 | ||
|
|
bf1081071b | ||
|
|
e261ce7554 | ||
|
|
7cae0ceab8 | ||
|
|
064ee91225 | ||
|
|
495b3ec770 | ||
|
|
39f9329207 | ||
|
|
2b72cfdaff | ||
|
|
70d32ea1aa | ||
|
|
0871482681 | ||
|
|
5d4f3eab06 | ||
|
|
fa3265b1fb | ||
|
|
92e6ffc7ef | ||
|
|
22f91f7aa2 | ||
|
|
43facd1e43 | ||
|
|
11242a2325 | ||
|
|
28db9a5262 | ||
|
|
57e8c6aafd | ||
|
|
fa47f63307 | ||
|
|
bac673f3ab | ||
|
|
45ac391998 | ||
|
|
fed5097708 | ||
|
|
9d115c30d7 | ||
|
|
a769da62c7 | ||
|
|
e20edab98f | ||
|
|
4f4fcb84ce | ||
|
|
6560d161c9 | ||
|
|
7f23c590ca | ||
|
|
0ca33eddb1 | ||
|
|
45341c4a31 | ||
|
|
5de5ae4ca2 | ||
|
|
03f71f3cdf | ||
|
|
f97ad66026 | ||
|
|
31392e5852 | ||
|
|
77a5a685f1 | ||
|
|
2b77e59e0a | ||
|
|
70f755599a | ||
|
|
2a76b2a5dd | ||
|
|
ffea243eae | ||
|
|
a4012e6100 | ||
|
|
9bd250f9fc | ||
|
|
7f93fb6b4d | ||
|
|
1b3f27852d | ||
|
|
fab29088e6 | ||
|
|
9feea75bdb | ||
|
|
52f08c818f | ||
|
|
e89483e3f2 | ||
|
|
467469f26d | ||
|
|
aa9d526207 | ||
|
|
3338858420 | ||
|
|
3e758c6a1c | ||
|
|
fc7e69523c | ||
|
|
3289b4411a | ||
|
|
5c98512241 | ||
|
|
3ca22933f4 | ||
|
|
106d3f3c74 | ||
|
|
15d3fbc02e | ||
|
|
eaae7e9ea2 | ||
|
|
9eb1c5047b | ||
|
|
25d4b603be | ||
|
|
eac88b5ef7 | ||
|
|
734b376e05 | ||
|
|
2354affde7 | ||
|
|
11e922b1a9 | ||
|
|
b7f0c3cc6c | ||
|
|
f9f629299b | ||
|
|
645c9d5900 | ||
|
|
6af2d38a98 | ||
|
|
dba8e80868 | ||
|
|
324ff2ab67 | ||
|
|
5ea09a5b0a | ||
|
|
c046caa894 | ||
|
|
be8197825c | ||
|
|
d48c28bec7 | ||
|
|
b07c1802a0 | ||
|
|
7f0cf1655a | ||
|
|
0a154dc79f | ||
|
|
7022e5bf49 | ||
|
|
86ae18564d | ||
|
|
a07a04d115 | ||
|
|
51e7c889ec | ||
|
|
4cd2381a5e | ||
|
|
6474240d49 | ||
|
|
0938bf3246 | ||
|
|
aad94f1b2a | ||
|
|
a677575eef | ||
|
|
9160e01333 | ||
|
|
d2b9fe475b | ||
|
|
cff9ae6b18 | ||
|
|
4fb49820af | ||
|
|
1f465ddddb | ||
|
|
1bda102d46 | ||
|
|
8066a7ed0f | ||
|
|
bc960ffde1 | ||
|
|
cbbbf44f1f | ||
|
|
28ab0d118c | ||
|
|
15710cbec9 | ||
|
|
4aaeee64d0 | ||
|
|
ffd3882aa8 | ||
|
|
36a90c4e76 | ||
|
|
098580ccf4 | ||
|
|
c0f8c9b9e4 | ||
|
|
a1a553eb52 | ||
|
|
18e1e7716c | ||
|
|
081d3a73fa | ||
|
|
4234a6440d | ||
|
|
a09275c582 | ||
|
|
a8099a1284 | ||
|
|
2f19b48f97 | ||
|
|
abbf7bdd69 | ||
|
|
6cf8c7d4f0 | ||
|
|
c6196f9900 | ||
|
|
cd7850337b | ||
|
|
0220965ca9 | ||
|
|
bae1512235 | ||
|
|
71f8a3ad3e | ||
|
|
24dd065839 | ||
|
|
08f0150177 | ||
|
|
367a4fc64e | ||
|
|
33ace8554b | ||
|
|
94cbb19db0 | ||
|
|
ea33c7b1b9 | ||
|
|
cba5fd4c27 | ||
|
|
297999a00e | ||
|
|
cbbf13e9e2 | ||
|
|
4437706494 | ||
|
|
3fff023a8a | ||
|
|
583429b6d9 | ||
|
|
61c12ca871 | ||
|
|
e3145b79d5 | ||
|
|
1d8f789c21 | ||
|
|
eb8e36e1eb | ||
|
|
21d8d93bed | ||
|
|
f2e09c020b | ||
|
|
ebe5647066 | ||
|
|
5b5e831e37 | ||
|
|
8a155d5fe3 | ||
|
|
2c67b82f4a | ||
|
|
7a2878657d | ||
|
|
90f9dd2bde | ||
|
|
6c292caa62 | ||
|
|
d3e73c4fc2 | ||
|
|
bfe0b218de | ||
|
|
0f44702640 | ||
|
|
8185175d14 | ||
|
|
8c24209e55 | ||
|
|
5c80b75d09 | ||
|
|
79c00dfa6c | ||
|
|
3793300d45 | ||
|
|
a70cd792c8 | ||
|
|
7c479c1df6 | ||
|
|
efd074a1ee | ||
|
|
7d27440003 | ||
|
|
269f7bf789 | ||
|
|
c96bfab70f | ||
|
|
ec63456b9a | ||
|
|
3a39eb3815 | ||
|
|
ce0ab8bd07 | ||
|
|
289496d6d2 | ||
|
|
abe45bccae | ||
|
|
a8893bdc96 | ||
|
|
cb76956887 | ||
|
|
809b971b4f | ||
|
|
d5f2f0e3af | ||
|
|
301d9de18e | ||
|
|
dcb1d920eb | ||
|
|
fabe476397 | ||
|
|
3101660a3e | ||
|
|
f8b677a2a9 | ||
|
|
15643f8dd5 | ||
|
|
fbaa0d667f | ||
|
|
50c5a2e8d9 | ||
|
|
7bc559acab | ||
|
|
c58a1aaa48 | ||
|
|
385c8c6ef1 | ||
|
|
d3457ce5b6 | ||
|
|
7087f783e6 | ||
|
|
8c8650090d | ||
|
|
12804ff1a0 | ||
|
|
4cb271868b | ||
|
|
9d546904e6 | ||
|
|
092b51f8f3 | ||
|
|
4a49519869 | ||
|
|
aa121b900e | ||
|
|
9e3ce4f1ec | ||
|
|
2e5385ca32 | ||
|
|
f5dfe31fdd | ||
|
|
032974e9ce | ||
|
|
31c086e2f0 | ||
|
|
3c4de2aecd | ||
|
|
8d9543c01e | ||
|
|
608d82ce4f | ||
|
|
7f0ed374b5 | ||
|
|
c7c9c95fbb | ||
|
|
5667da9ad2 | ||
|
|
558d0f1a4e | ||
|
|
0e832a7c41 | ||
|
|
7f948bf263 | ||
|
|
d4a32fd39d | ||
|
|
7bd5fc7ae0 | ||
|
|
36e8a4bf62 | ||
|
|
5ab01eb537 | ||
|
|
451ef2e734 | ||
|
|
2e657940bc | ||
|
|
6ef542837a | ||
|
|
619126bd24 | ||
|
|
1bdc49e087 | ||
|
|
cf4ee53f0d | ||
|
|
d742aa2f48 | ||
|
|
b7f693b934 | ||
|
|
c5ede32649 | ||
|
|
f543cf2da5 | ||
|
|
40cdb0c507 | ||
|
|
7a8b3d08df | ||
|
|
12a898476e | ||
|
|
2acf043872 | ||
|
|
062395dac1 | ||
|
|
6fc76d4122 | ||
|
|
859df96afc | ||
|
|
b95da484a0 | ||
|
|
d6d6b73a38 | ||
|
|
c8b8fba171 | ||
|
|
4580d6f710 | ||
|
|
2a19a423de | ||
|
|
409c1eef30 | ||
|
|
6c479808d0 | ||
|
|
bd14be485a | ||
|
|
fbf362a91f | ||
|
|
82704b60c7 | ||
|
|
b92feb382b | ||
|
|
66f934bba7 | ||
|
|
13366b9985 | ||
|
|
e971733d51 | ||
|
|
3bd491151b | ||
|
|
580bc65c3e | ||
|
|
d2984548a7 | ||
|
|
4d3090a590 | ||
|
|
53fa79b96c | ||
|
|
293bdaedfe | ||
|
|
7498e5d6f7 | ||
|
|
213049b52e | ||
|
|
6456aad16d | ||
|
|
8a3b313cb6 | ||
|
|
ab28086779 | ||
|
|
965fcec9df | ||
|
|
1593eacb6b | ||
|
|
abb5ae653c | ||
|
|
bbf360b569 | ||
|
|
1066a09612 | ||
|
|
5e1d33f7f4 | ||
|
|
2813ea056b | ||
|
|
a8638e9bc8 | ||
|
|
74dc44546d | ||
|
|
f645b86963 | ||
|
|
7c30e29adf | ||
|
|
19c97b570e | ||
|
|
7a210b4ee0 | ||
|
|
fdf3aa471c | ||
|
|
2851c8a9cf | ||
|
|
49d4324992 | ||
|
|
7648be7937 | ||
|
|
1dea908152 | ||
|
|
896f15222c | ||
|
|
540ced5bfc | ||
|
|
85ec235911 | ||
|
|
a810467200 | ||
|
|
354d03c38d | ||
|
|
83b17ee05b | ||
|
|
cb2da78cbf | ||
|
|
2fd8d1991b | ||
|
|
8c80200fc0 | ||
|
|
b639ac850f | ||
|
|
2ef015015a | ||
|
|
7921b67624 | ||
|
|
b8f735970e | ||
|
|
5181fb38c4 | ||
|
|
7439bc56b9 | ||
|
|
b31cd307e3 | ||
|
|
e2e226471a | ||
|
|
f2efe234ea | ||
|
|
3d172f2726 | ||
|
|
8b9dba2f97 | ||
|
|
e0bdd38e6f | ||
|
|
579fa88070 | ||
|
|
a541db8487 | ||
|
|
83bdbb4c94 | ||
|
|
9098450a7a | ||
|
|
e0596595ed | ||
|
|
0da51cda89 | ||
|
|
7896368b36 | ||
|
|
915709519c | ||
|
|
6f6def88a3 | ||
|
|
65dbf03a12 | ||
|
|
78609613bc | ||
|
|
78acd8d118 | ||
|
|
f1969e783f | ||
|
|
3ad2429293 | ||
|
|
ae72a6f574 | ||
|
|
baf6144ee7 | ||
|
|
71e515f9c2 | ||
|
|
ab6bda36c3 | ||
|
|
d14790271d | ||
|
|
1cb55186d2 | ||
|
|
77da4052b9 | ||
|
|
a631890db1 | ||
|
|
b233313a47 | ||
|
|
6431f1948d | ||
|
|
a65df3ed9a | ||
|
|
fb28d6b927 | ||
|
|
0d82c3703d | ||
|
|
1d5a8a5948 | ||
|
|
aa1c8ae054 | ||
|
|
b976615489 | ||
|
|
c06480634a | ||
|
|
33d79c5d28 | ||
|
|
dbf5fc7e10 | ||
|
|
17d5068ec7 | ||
|
|
f4cc9ecc0d | ||
|
|
3c46c461c0 | ||
|
|
ee70fec7ad | ||
|
|
69c2a1b3c2 | ||
|
|
c2ababb9d6 | ||
|
|
db9049130c | ||
|
|
65b74d0483 | ||
|
|
c21083bf80 | ||
|
|
47c281aaa6 | ||
|
|
e73e5e0340 | ||
|
|
1cb38e279c | ||
|
|
d8500128ea | ||
|
|
27e042baf7 | ||
|
|
d7aa94d6ae | ||
|
|
c0007a9566 | ||
|
|
c45da95237 | ||
|
|
45d17728d4 | ||
|
|
c4c81742d5 | ||
|
|
4708509585 | ||
|
|
99b5f3cc3b | ||
|
|
dad3de9cd3 | ||
|
|
3566fc877a | ||
|
|
77ff0298f1 | ||
|
|
22301f5429 | ||
|
|
25a83adc78 | ||
|
|
9dc1328b47 | ||
|
|
5d28e5a959 | ||
|
|
4ff07d12a6 | ||
|
|
5688293288 | ||
|
|
9b77403796 | ||
|
|
c93c2fb6e8 | ||
|
|
5800babdab | ||
|
|
5e3600ac44 | ||
|
|
3af2342d7b | ||
|
|
3d68c83907 | ||
|
|
6366df2c24 | ||
|
|
05ca336c1b | ||
|
|
f643102696 | ||
|
|
33ef50daea | ||
|
|
8071207bf3 | ||
|
|
767bb27175 | ||
|
|
b7f240faf0 | ||
|
|
e0e2b2d7f7 | ||
|
|
10b515f1d1 | ||
|
|
d81c05c444 | ||
|
|
bc0a205f03 | ||
|
|
0e53ddc83b | ||
|
|
0400b577bb | ||
|
|
ec2085f125 | ||
|
|
6430427e3a | ||
|
|
38a1b6a417 | ||
|
|
764d7a2f1c | ||
|
|
e65564234f | ||
|
|
1ec18eb44a | ||
|
|
4c51c28d7a | ||
|
|
eaa134089e | ||
|
|
a015c9ca2a | ||
|
|
d8ca865fc6 | ||
|
|
987b02d733 | ||
|
|
3dd8d4349d | ||
|
|
fd43908e4c | ||
|
|
257ed8ebc3 | ||
|
|
dfd8bf1c0f | ||
|
|
0f709c2275 | ||
|
|
3b64e6046c | ||
|
|
26cbc24a10 | ||
|
|
33a5479809 | ||
|
|
000c64755d | ||
|
|
b32249d48b | ||
|
|
c325cc1120 | ||
|
|
8d2791b32e | ||
|
|
466fc15382 | ||
|
|
86cf3be225 | ||
|
|
dcf23aa0a0 | ||
|
|
36c823b14e | ||
|
|
8c905d22ea | ||
|
|
83692898ae | ||
|
|
5c7858c181 | ||
|
|
3aec460614 | ||
|
|
18159a1b77 | ||
|
|
216f931993 | ||
|
|
4587fed81b | ||
|
|
0642dcb7ba | ||
|
|
6267767ce7 | ||
|
|
3aa751aa10 | ||
|
|
053cfdf3a9 | ||
|
|
563583cd36 | ||
|
|
df1433786a | ||
|
|
7f6365cc81 | ||
|
|
56acb0929a | ||
|
|
a0831890ad | ||
|
|
da9aa3e133 | ||
|
|
91d99d5f14 | ||
|
|
83c6dd4d6b | ||
|
|
0ad13a4dbe | ||
|
|
3a2655e57d | ||
|
|
126fe34005 | ||
|
|
bd41c0e78e | ||
|
|
c49dec3835 | ||
|
|
66a7c7e6b8 | ||
|
|
50f048fa8b | ||
|
|
1035fa20e9 | ||
|
|
f4aaf2ad39 | ||
|
|
1dec993673 | ||
|
|
3c557e1ad6 | ||
|
|
3bb18f0440 | ||
|
|
c661d67f77 | ||
|
|
158d480bb3 | ||
|
|
759aa5a8ea | ||
|
|
5ea482456e | ||
|
|
e1a212156d | ||
|
|
9299ac5813 | ||
|
|
abfd8a7d86 |
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@@ -10,7 +10,9 @@ updates:
|
||||
schedule:
|
||||
interval: "daily"
|
||||
versioning-strategy: increase
|
||||
open-pull-requests-limit: 10
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/src/pretix/static/npm_dir"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
open-pull-requests-limit: 5
|
||||
|
||||
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -26,12 +26,12 @@ jobs:
|
||||
matrix:
|
||||
python-version: ["3.11"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
|
||||
6
.github/workflows/docs.yml
vendored
6
.github/workflows/docs.yml
vendored
@@ -25,12 +25,12 @@ jobs:
|
||||
name: Spellcheck
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.11
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
|
||||
12
.github/workflows/strings.yml
vendored
12
.github/workflows/strings.yml
vendored
@@ -23,12 +23,12 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
name: Check gettext syntax
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.11
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
@@ -48,12 +48,12 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
name: Spellcheck
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.11
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
|
||||
16
.github/workflows/style.yml
vendored
16
.github/workflows/style.yml
vendored
@@ -23,12 +23,12 @@ jobs:
|
||||
name: isort
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.11
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
@@ -43,12 +43,12 @@ jobs:
|
||||
name: flake8
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.11
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
@@ -63,9 +63,9 @@ jobs:
|
||||
name: licenseheaders
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.11
|
||||
- name: Install Dependencies
|
||||
|
||||
6
.github/workflows/tests.yml
vendored
6
.github/workflows/tests.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
- database: sqlite
|
||||
python-version: "3.10"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: harmon758/postgresql-action@v1
|
||||
with:
|
||||
postgresql version: '15'
|
||||
@@ -41,10 +41,10 @@ jobs:
|
||||
postgresql password: 'postgres'
|
||||
if: matrix.database == 'postgres'
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
|
||||
@@ -42,7 +42,6 @@ Example::
|
||||
currency=EUR
|
||||
datadir=/data
|
||||
plugins_default=pretix.plugins.sendmail,pretix.plugins.statistics
|
||||
cookie_domain=.pretix.de
|
||||
|
||||
``instance_name``
|
||||
The name of this installation. Default: ``pretix.de``
|
||||
@@ -53,10 +52,18 @@ Example::
|
||||
``currency``
|
||||
The default currency as a three-letter code. Defaults to ``EUR``.
|
||||
|
||||
``cachedir``
|
||||
The local path to a directory where temporary files will be stored.
|
||||
Defaults to the ``cache`` directory below the ``datadir``.
|
||||
|
||||
``datadir``
|
||||
The local path to a data directory that will be used for storing user uploads and similar
|
||||
data. Defaults to the value of the environment variable ``DATA_DIR`` or ``data``.
|
||||
|
||||
``logdir``
|
||||
The local path to a directory where log files will be stored.
|
||||
Defaults to the ``logs`` directory below the ``datadir``.
|
||||
|
||||
``plugins_default``
|
||||
A comma-separated list of plugins that are enabled by default for all new events.
|
||||
Defaults to ``pretix.plugins.sendmail,pretix.plugins.statistics``.
|
||||
@@ -71,11 +78,8 @@ Example::
|
||||
``auth_backends``
|
||||
A comma-separated list of available auth backends. Defaults to ``pretix.base.auth.NativeAuthBackend``.
|
||||
|
||||
``cookie_domain``
|
||||
The cookie domain to be set. Defaults to ``None``.
|
||||
|
||||
``registration``
|
||||
Enables or disables the registration of new admin users. Defaults to ``on``.
|
||||
Enables or disables the registration of new admin users. Defaults to ``off``.
|
||||
|
||||
``password_reset``
|
||||
Enables or disables password reset. Defaults to ``on``.
|
||||
@@ -93,8 +97,9 @@ Example::
|
||||
Defaults to ``off``.
|
||||
|
||||
``obligatory_2fa``
|
||||
Enables or disables obligatory usage of Two-Factor Authentication for users of the pretix backend.
|
||||
Defaults to ``False``
|
||||
Enables or disables obligatory usage of two-factor authentication for users of the pretix backend.
|
||||
Can be ``True`` to make two-factor authentication obligatory for all users or ``staff`` to make it only
|
||||
obligatory to users with admin permissions. Defaults to ``False``.
|
||||
|
||||
``trust_x_forwarded_for``
|
||||
Specifies whether the ``X-Forwarded-For`` header can be trusted. Only set to ``on`` if you have a reverse
|
||||
@@ -153,6 +158,7 @@ Example::
|
||||
host=localhost
|
||||
port=3306
|
||||
advisory_lock_index=1
|
||||
disable_server_side_cursors=0
|
||||
sslmode=require
|
||||
sslrootcert=/etc/pretix/postgresql-ca.crt
|
||||
sslcert=/etc/pretix/postgresql-client-crt.crt
|
||||
@@ -173,6 +179,11 @@ Example::
|
||||
and are not scoped to a specific database. If you run multiple pretix applications with the same PostgreSQL server,
|
||||
you should set separate values for this setting (integers up to 256).
|
||||
|
||||
``disable_server_side_cursors``
|
||||
On PostgreSQL pretix might use server side cursors for certain operations. This is generally fine but will break in
|
||||
specific circumstances, for example when connecting to PostgreSQL through a PGBouncer configured with a transaction
|
||||
pool mode. Off by default (i.e. by default server side cursors will be used).
|
||||
|
||||
``sslmode``, ``sslrootcert``
|
||||
Connection TLS details for the PostgreSQL database connection. Possible values of ``sslmode`` are ``disable``, ``allow``, ``prefer``, ``require``, ``verify-ca``, and ``verify-full``. ``sslrootcert`` should be the accessible path of the ca certificate. Both values are empty by default.
|
||||
|
||||
@@ -349,7 +360,7 @@ to speed up various operations::
|
||||
The location of redis, as a URL of the form ``redis://[:password]@localhost:6379/0``
|
||||
or ``unix://[:password]@/path/to/socket.sock?db=0``
|
||||
|
||||
``session``
|
||||
``sessions``
|
||||
When this is set to ``True``, redis will be used as the session storage.
|
||||
|
||||
``sentinels``
|
||||
@@ -525,4 +536,4 @@ pretix can optionally make use of a GeoIP database for some features. It needs a
|
||||
|
||||
|
||||
.. _GeoAcumen: https://github.com/geoacumen/geoacumen-country
|
||||
.. _GeoLite2: https://dev.maxmind.com/geoip/geolite2-free-geolocation-data
|
||||
.. _GeoLite2: https://dev.maxmind.com/geoip/geolite2-free-geolocation-data
|
||||
|
||||
13
doc/admin/installation/community.rst
Normal file
13
doc/admin/installation/community.rst
Normal file
@@ -0,0 +1,13 @@
|
||||
.. highlight:: none
|
||||
|
||||
.. _`community`:
|
||||
|
||||
Community install guides
|
||||
================================
|
||||
|
||||
.. warning:: The guides are maintained by the community and not by the pretix core team. If you encounter any issues with the guides, please report them to the maintainers of the guides. The pretix core team can not provide support for installs using these guides.
|
||||
|
||||
Kubernetes
|
||||
------------
|
||||
- Helm Chart by techwolf12 - A Helm chart for deploying pretix on Kubernetes. The chart documentation is available on `ArtifactHub <https://artifacthub.io/packages/helm/techwolf12/pretix>`_ and the source code is available on `GitHub <https://github.com/Techwolf12/charts/tree/main/pretix-helm>`_.
|
||||
|
||||
@@ -276,7 +276,8 @@ Restarting the service can take a few seconds, especially if the update requires
|
||||
Replace ``stable`` above with a specific version number like ``1.0`` or with ``latest`` for the development
|
||||
version, if you want to.
|
||||
|
||||
Make sure to also read :ref:`update_notes` and the release notes of the version you are updating to.
|
||||
Make sure to also read :ref:`update_notes` and the release notes of the version you are updating to. Pay special
|
||||
attention to the "Runtime and server environment" section of all release notes between your current and new version.
|
||||
|
||||
.. _`docker_plugininstall`:
|
||||
|
||||
|
||||
@@ -14,3 +14,4 @@ for your needs.
|
||||
manual_smallscale
|
||||
dev_version
|
||||
enterprise
|
||||
community
|
||||
|
||||
@@ -120,6 +120,7 @@ Now we will install pretix itself. The following steps are to be executed as the
|
||||
actually install pretix, we will create a virtual environment to isolate the python packages from your global
|
||||
python installation::
|
||||
|
||||
# sudo -u pretix -s
|
||||
$ python3 -m venv /var/pretix/venv
|
||||
$ source /var/pretix/venv/bin/activate
|
||||
(venv)$ pip3 install -U pip setuptools wheel
|
||||
@@ -279,6 +280,7 @@ Updates
|
||||
|
||||
To upgrade to a new pretix release, pull the latest code changes and run the following commands::
|
||||
|
||||
# sudo -u pretix -s
|
||||
$ source /var/pretix/venv/bin/activate
|
||||
(venv)$ pip3 install -U --upgrade-strategy eager pretix gunicorn
|
||||
(venv)$ python -m pretix migrate
|
||||
@@ -286,7 +288,8 @@ To upgrade to a new pretix release, pull the latest code changes and run the fol
|
||||
(venv)$ python -m pretix updatestyles
|
||||
# systemctl restart pretix-web pretix-worker
|
||||
|
||||
Make sure to also read :ref:`update_notes` and the release notes of the version you are updating to.
|
||||
Make sure to also read :ref:`update_notes` and the release notes of the version you are updating to. Pay special
|
||||
attention to the "Runtime and server environment" section of all release notes between your current and new version.
|
||||
|
||||
.. _`manual_plugininstall`:
|
||||
|
||||
|
||||
@@ -103,6 +103,12 @@ pretix_celery_tasks_queued_count
|
||||
pretix_celery_tasks_queued_age_seconds
|
||||
The age of the longest-waiting in the worker queue in seconds, labeled with ``queue``.
|
||||
|
||||
pretix_successful_logins
|
||||
Counter. The number of successful backend logins.
|
||||
|
||||
pretix_failed_logins
|
||||
Counter. The number of failed backend logins, labeled with ``reason``.
|
||||
|
||||
.. _metric types: https://prometheus.io/docs/concepts/metric_types/
|
||||
.. _Prometheus: https://prometheus.io/
|
||||
.. _cProfile: https://docs.python.org/3/library/profile.html
|
||||
|
||||
@@ -47,5 +47,30 @@ Or, with a docker installation::
|
||||
|
||||
$ docker exec -it pretix.service pretix create_order_transactions
|
||||
|
||||
Upgrade to 2023.6.0 or newer
|
||||
""""""""""""""""""""""""""""
|
||||
|
||||
MariaDB and MySQL are no longer supported.
|
||||
|
||||
Upgrade to 2023.8.0 or newer
|
||||
""""""""""""""""""""""""""""
|
||||
|
||||
PostgreSQL 11 is now required.
|
||||
|
||||
Upgrade to 2023.9.0 or newer
|
||||
""""""""""""""""""""""""""""
|
||||
|
||||
This release includes a migration that changes the `id` column of all core database tables from `integer`
|
||||
to `bigint`. If you have a large database, the migration step of the upgrade might take significantly longer than
|
||||
usual, so plan the update accordingly.
|
||||
|
||||
The default value for the `registration` setting in `pretix.cfg` has changed to `false`.
|
||||
|
||||
Upgrade to 2023.10.0 or newer
|
||||
"""""""""""""""""""""""""""""
|
||||
|
||||
This release includes a migration that changes retroactively fills an `organizer` column in the table
|
||||
`pretixbase_logentry`. If you have a large database, the migration step of the upgrade might take significantly
|
||||
longer than usual, so plan the update accordingly.
|
||||
|
||||
.. _blog: https://pretix.eu/about/en/blog/
|
||||
|
||||
@@ -249,7 +249,10 @@ You can get three response codes:
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"event": "democon",
|
||||
"event": {
|
||||
"name": "Demo Conference",
|
||||
"slug": "democon"
|
||||
},
|
||||
"subevent": 23,
|
||||
"checkinlist": 5
|
||||
}
|
||||
|
||||
@@ -94,7 +94,9 @@ If you want the user to return to your application after the payment is complete
|
||||
"Plugins". Enable the plugin "Redirection from order page". Then, go to the new page "Settings", then "Redirection".
|
||||
Enter the base URL of your web application. This will allow you to redirect to pages under this base URL later on.
|
||||
For example, if you want users to be redirected to ``https://example.org/order/return?tx_id=1234``, you could now
|
||||
either enter ``https://example.org`` or ``https://example.org/order/``.
|
||||
either enter ``https://example.org/order/`` or ``https://example.org/``.
|
||||
Please note that in the latter case the trailing slash is required, ``https://example.org`` is not allowed to prevent.
|
||||
Only base URLs with a secure (``https://``) or local (``http://localhost``) origin are permitted.
|
||||
|
||||
The user will be redirected back to your page instead of pretix' order confirmation page after the payment,
|
||||
**regardless of whether it was successful or not**. We will append an ``error=…`` query parameter with an error
|
||||
|
||||
@@ -31,6 +31,7 @@ Checking a ticket in
|
||||
This endpoint supports passing multiple check-in lists to perform a multi-event scan. However, each check-in list
|
||||
passed needs to be from a distinct event.
|
||||
|
||||
:query string expand: Expand a field inside the ``position`` object into a full object. Currently ``subevent``, ``item``, ``variation``, and ``answers.question`` are supported. Can be passed multiple times.
|
||||
:<json string secret: Scanned QR code corresponding to the ``secret`` attribute of a ticket.
|
||||
:<json string source_type: Type of source the ``secret`` was obtained form. Defaults to ``"barcode"``.
|
||||
:<json array lists: List of check-in list IDs to search on. No two check-in lists may be from the same event.
|
||||
@@ -63,6 +64,7 @@ Checking a ticket in
|
||||
``checkin_attention`` flag set. (3) If ``attendee_name`` is empty, it may automatically fall
|
||||
back to values from a parent product or from invoice addresses.
|
||||
:>json boolean require_attention: Whether or not the ``require_attention`` flag is set on the item or order.
|
||||
:>json list checkin_texts: List of additional texts to show to the user.
|
||||
:>json object list: Excerpt of information about the matching :ref:`check-in list <rest-checkinlists>` (if any was found),
|
||||
including the attributes ``id``, ``name``, ``event``, ``subevent``, and ``include_pending``.
|
||||
:>json object questions: List of questions to be answered for check-in, only set on status ``"incomplete"``.
|
||||
@@ -103,6 +105,7 @@ Checking a ticket in
|
||||
…
|
||||
},
|
||||
"require_attention": false,
|
||||
"checkin_texts": [],
|
||||
"list": {
|
||||
"id": 1,
|
||||
"name": "Default check-in list",
|
||||
@@ -125,6 +128,7 @@ Checking a ticket in
|
||||
…
|
||||
},
|
||||
"require_attention": false,
|
||||
"checkin_texts": [],
|
||||
"list": {
|
||||
"id": 1,
|
||||
"name": "Default check-in list",
|
||||
@@ -142,6 +146,7 @@ Checking a ticket in
|
||||
"position": 1,
|
||||
"identifier": "WY3TP9SL",
|
||||
"ask_during_checkin": true,
|
||||
"show_during_checkin": true,
|
||||
"options": [
|
||||
{
|
||||
"id": 1,
|
||||
@@ -178,7 +183,8 @@ Checking a ticket in
|
||||
"status": "error",
|
||||
"reason": "invalid",
|
||||
"reason_explanation": null,
|
||||
"require_attention": false
|
||||
"require_attention": false,
|
||||
"checkin_texts": []
|
||||
}
|
||||
|
||||
**Example error response (known, but invalid ticket)**:
|
||||
@@ -193,6 +199,7 @@ Checking a ticket in
|
||||
"reason": "unpaid",
|
||||
"reason_explanation": null,
|
||||
"require_attention": false,
|
||||
"checkin_texts": [],
|
||||
"list": {
|
||||
"id": 1,
|
||||
"name": "Default check-in list",
|
||||
@@ -217,6 +224,7 @@ Checking a ticket in
|
||||
* ``rules`` - Check-in prevented by a user-defined rule.
|
||||
* ``ambiguous`` - Multiple tickets match scan, rejected.
|
||||
* ``revoked`` - Ticket code has been revoked.
|
||||
* ``unapproved`` - Order has not yet been approved.
|
||||
* ``error`` - Internal error.
|
||||
|
||||
In case of reason ``rules`` and ``invalid_time``, there might be an additional response field ``reason_explanation``
|
||||
|
||||
@@ -37,12 +37,18 @@ allow_entry_after_exit boolean If ``true``, su
|
||||
rules object Custom check-in logic. The contents of this field are currently not considered a stable API and modifications through the API are highly discouraged.
|
||||
exit_all_at datetime Automatically check out (i.e. perform an exit scan) at this point in time. After this happened, this property will automatically be set exactly one day into the future. Note that this field is considered "internal configuration" and if you pull the list with ``If-Modified-Since``, the daily change in this field will not trigger a response.
|
||||
addon_match boolean If ``true``, tickets on this list can be redeemed by scanning their parent ticket if this still leads to an unambiguous match.
|
||||
ignore_in_statistics boolean If ``true``, check-ins on this list will be ignored in most reporting features.
|
||||
consider_tickets_used boolean If ``true`` (default), tickets checked in on this list will be considered "used" by other functionality, i.e. when checking if they can still be canceled.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 4.12
|
||||
|
||||
The ``addon_match`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 2023.9
|
||||
|
||||
The ``ignore_in_statistics`` and ``consider_tickets_used`` attributes have been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -492,7 +498,7 @@ Order position endpoints
|
||||
``attendee_name,positionid``
|
||||
:query string order: Only return positions of the order with the given order code
|
||||
:query string search: Fuzzy search matching the attendee name, order code, invoice address name as well as to the beginning of the secret.
|
||||
:query string expand: Expand a field into a full object. Currently only ``subevent``, ``item``, and ``variation`` are supported. Can be passed multiple times.
|
||||
:query string expand: Expand a field into a full object. Currently ``subevent``, ``item``, ``variation``, and ``answers.question`` are supported. Can be passed multiple times.
|
||||
:query integer item: Only return positions with the purchased item matching the given ID.
|
||||
:query integer item__in: Only return positions with the purchased item matching one of the given comma-separated IDs.
|
||||
:query integer variation: Only return positions with the purchased item variation matching the given ID.
|
||||
@@ -626,7 +632,8 @@ Order position endpoints
|
||||
set this to ``false``. In that case, questions will just be ignored. Defaults
|
||||
to ``true``.
|
||||
:<json boolean canceled_supported: When this parameter is set to ``true``, the response code ``canceled`` may be
|
||||
returned. Otherwise, canceled orders will return ``unpaid``.
|
||||
returned. Otherwise, canceled orders will return ``unpaid``. (**Deprecated**, in
|
||||
the future, this will be ignored and ``canceled`` may always be returned.)
|
||||
:<json datetime datetime: Specifies the datetime of the check-in. If not supplied, the current time will be used.
|
||||
:<json boolean force: Specifies that the check-in should succeed regardless of revoked barcode, previous check-ins or required
|
||||
questions that have not been filled. This is usually used to upload offline scans that already happened,
|
||||
@@ -700,6 +707,7 @@ Order position endpoints
|
||||
"position": 1,
|
||||
"identifier": "WY3TP9SL",
|
||||
"ask_during_checkin": true,
|
||||
"show_during_checkin": true,
|
||||
"options": [
|
||||
{
|
||||
"id": 1,
|
||||
@@ -752,6 +760,7 @@ Order position endpoints
|
||||
* ``rules`` - Check-in prevented by a user-defined rule.
|
||||
* ``ambiguous`` - Multiple tickets match scan, rejected.
|
||||
* ``revoked`` - Ticket code has been revoked.
|
||||
* ``unapproved`` - Order has not yet been approved.
|
||||
|
||||
In case of reason ``rules`` or ``invalid_time``, there might be an additional response field ``reason_explanation``
|
||||
with a human-readable description of the violated rules. However, that field can also be missing or be ``null``.
|
||||
@@ -767,4 +776,4 @@ Order position endpoints
|
||||
:statuscode 404: The requested order position or check-in list does not exist.
|
||||
|
||||
|
||||
.. _security issues: https://pretix.eu/about/de/blog/20220705-release-4111/
|
||||
.. _security issues: https://pretix.eu/about/de/blog/20220705-release-4111/
|
||||
|
||||
@@ -19,6 +19,7 @@ external_identifier string External ID of
|
||||
the API, but is read-only for customers created through a
|
||||
SSO integration.
|
||||
email string Customer email address
|
||||
phone string Customer phone number
|
||||
name string Name of this customer (or ``null``)
|
||||
name_parts object of strings Decomposition of name (i.e. given name, family name)
|
||||
is_active boolean Whether this account is active
|
||||
@@ -39,6 +40,10 @@ password string Can only be set
|
||||
|
||||
Passwords can now be set through the API during customer creation.
|
||||
|
||||
.. versionchanged:: 2024.3
|
||||
|
||||
The attribute ``phone`` has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -71,6 +76,7 @@ Endpoints
|
||||
"identifier": "8WSAJCJ",
|
||||
"external_identifier": null,
|
||||
"email": "customer@example.org",
|
||||
"phone": "+493012345678",
|
||||
"name": "John Doe",
|
||||
"name_parts": {
|
||||
"_scheme": "full",
|
||||
@@ -118,6 +124,7 @@ Endpoints
|
||||
"identifier": "8WSAJCJ",
|
||||
"external_identifier": null,
|
||||
"email": "customer@example.org",
|
||||
"phone": "+493012345678",
|
||||
"name": "John Doe",
|
||||
"name_parts": {
|
||||
"_scheme": "full",
|
||||
@@ -155,6 +162,7 @@ Endpoints
|
||||
|
||||
{
|
||||
"email": "test@example.org",
|
||||
"phone": "+493012345678",
|
||||
"password": "verysecret",
|
||||
"send_email": true
|
||||
}
|
||||
@@ -171,6 +179,7 @@ Endpoints
|
||||
"identifier": "8WSAJCJ",
|
||||
"external_identifier": null,
|
||||
"email": "test@example.org",
|
||||
"phone": "+493012345678",
|
||||
...
|
||||
}
|
||||
|
||||
@@ -215,6 +224,7 @@ Endpoints
|
||||
"identifier": "8WSAJCJ",
|
||||
"external_identifier": null,
|
||||
"email": "test@example.org",
|
||||
"phone": "+493012345678",
|
||||
…
|
||||
}
|
||||
|
||||
@@ -249,6 +259,7 @@ Endpoints
|
||||
"identifier": "8WSAJCJ",
|
||||
"external_identifier": null,
|
||||
"email": null,
|
||||
"phone": null,
|
||||
…
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ geo_lon float Longitude of th
|
||||
has_subevents boolean ``true`` if the event series feature is active for this
|
||||
event. Cannot change after event is created.
|
||||
meta_data object Values set for organizer-specific meta data parameters.
|
||||
The allowed keys need to be set up as meta properties
|
||||
in the organizer configuration.
|
||||
plugins list A list of package names of the enabled plugins for this
|
||||
event.
|
||||
seating_plan integer If reserved seating is in use, the ID of a seating
|
||||
@@ -343,8 +345,8 @@ Endpoints
|
||||
Creates a new event with properties as set in the request body. The properties that are copied are: ``is_public``,
|
||||
``testmode``, ``has_subevents``, settings, plugin settings, items, variations, add-ons, quotas, categories, tax rules, questions.
|
||||
|
||||
If the ``plugins``, ``has_subevents`` and/or ``is_public`` fields are present in the post body this will determine their
|
||||
value. Otherwise their value will be copied from the existing event.
|
||||
If the ``plugins``, ``has_subevents``, ``meta_data`` and/or ``is_public`` fields are present in the post body this will
|
||||
determine their value. Otherwise their value will be copied from the existing event.
|
||||
|
||||
Please note that you can only copy from events under the same organizer this way. Use the ``clone_from`` parameter
|
||||
when creating a new event for this instead.
|
||||
@@ -565,6 +567,8 @@ organizer level.
|
||||
.. 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.
|
||||
|
||||
.. note:: When authenticating with :ref:`rest-deviceauth`, only a limited subset of settings is available.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/settings/
|
||||
|
||||
Get current values of event settings.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
.. spelling:word-list:: checkin
|
||||
|
||||
.. _rest-exporters:
|
||||
|
||||
Data exporters
|
||||
==============
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ at :ref:`plugin-docs`.
|
||||
webhooks
|
||||
seatingplans
|
||||
exporters
|
||||
scheduled_exports
|
||||
shredders
|
||||
sendmail_rules
|
||||
billing_invoices
|
||||
|
||||
@@ -18,6 +18,8 @@ default_price money (string) The price set d
|
||||
price money (string) The price used for this variation. This is either the
|
||||
same as ``default_price`` if that value is set or equal
|
||||
to the item's ``default_price`` (read-only).
|
||||
free_price_suggestion money (string) A suggested price, used as a default value if
|
||||
``Item.free_price`` is set (or ``null``).
|
||||
original_price money (string) An original price, shown for comparison, not used
|
||||
for price calculations (or ``null``).
|
||||
active boolean If ``false``, this variation will not be sold or shown.
|
||||
@@ -27,6 +29,8 @@ position integer An integer, use
|
||||
checkin_attention boolean If ``true``, the check-in app should show a warning
|
||||
that this ticket requires special attention if such
|
||||
a variation is being scanned.
|
||||
checkin_text string Text that will be shown if a ticket of this type is
|
||||
scanned (or ``null``).
|
||||
require_approval boolean If ``true``, orders with this variation will need to be
|
||||
approved by the event organizer before they can be
|
||||
paid.
|
||||
@@ -41,8 +45,16 @@ sales_channels list of strings Sales channels
|
||||
available.
|
||||
available_from datetime The first date time at which this variation can be bought
|
||||
(or ``null``).
|
||||
available_from_mode string If ``hide`` (the default), this variation is hidden in the shop
|
||||
if unavailable due to the available_from setting.
|
||||
If ``info``, the variation is visible, but can't be purchased,
|
||||
and a note explaining the unavailability is displayed.
|
||||
available_until datetime The last date time at which this variation can be bought
|
||||
(or ``null``).
|
||||
available_until_mode string If ``hide`` (the default), this variation is hidden in the shop
|
||||
if unavailable due to the available_until setting.
|
||||
If ``info``, the variation is visible, but can't be purchased,
|
||||
and a note explaining the unavailability is displayed.
|
||||
hide_without_voucher boolean If ``true``, this variation is only shown during the voucher
|
||||
redemption process, but not in the normal shop
|
||||
frontend.
|
||||
@@ -53,6 +65,12 @@ meta_data object Values set for
|
||||
|
||||
The ``meta_data`` and ``checkin_attention`` attributes have been added.
|
||||
|
||||
.. versionchanged:: 2023.10
|
||||
|
||||
The ``free_price_suggestion`` attribute has been added.
|
||||
The ``checkin_text`` attribute has been added.
|
||||
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -88,13 +106,16 @@ Endpoints
|
||||
},
|
||||
"active": true,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": false,
|
||||
"description": {
|
||||
"en": "Test2"
|
||||
@@ -103,6 +124,7 @@ Endpoints
|
||||
"default_price": "223.00",
|
||||
"price": 223.0,
|
||||
"original_price": null,
|
||||
"free_price_suggestion": null,
|
||||
"meta_data": {}
|
||||
},
|
||||
{
|
||||
@@ -112,14 +134,23 @@ Endpoints
|
||||
},
|
||||
"active": true,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": false,
|
||||
"description": {},
|
||||
"position": 1,
|
||||
"default_price": null,
|
||||
"price": 15.0,
|
||||
"default_price": "223.00",
|
||||
"price": 223.0,
|
||||
"original_price": null,
|
||||
"free_price_suggestion": null,
|
||||
"meta_data": {}
|
||||
}
|
||||
]
|
||||
@@ -163,15 +194,19 @@ Endpoints
|
||||
"default_price": "10.00",
|
||||
"price": "10.00",
|
||||
"original_price": null,
|
||||
"free_price_suggestion": null,
|
||||
"active": true,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"position": 0,
|
||||
@@ -204,13 +239,16 @@ Endpoints
|
||||
"default_price": "10.00",
|
||||
"active": true,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"position": 0,
|
||||
@@ -231,15 +269,19 @@ Endpoints
|
||||
"default_price": "10.00",
|
||||
"price": "10.00",
|
||||
"original_price": null,
|
||||
"free_price_suggestion": null,
|
||||
"active": true,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"position": 0,
|
||||
@@ -291,15 +333,19 @@ Endpoints
|
||||
"default_price": "10.00",
|
||||
"price": "10.00",
|
||||
"original_price": null,
|
||||
"free_price_suggestion": null,
|
||||
"active": false,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"position": 1,
|
||||
|
||||
@@ -29,6 +29,8 @@ free_price boolean If ``true``,
|
||||
they buy the product (however, the price can't be set
|
||||
lower than the price defined by ``default_price`` or
|
||||
otherwise).
|
||||
free_price_suggestion money (string) A suggested price, used as a default value if
|
||||
``free_price`` is set (or ``null``).
|
||||
tax_rate decimal (string) The VAT rate to be applied for this item (read-only,
|
||||
set through ``tax_rule``).
|
||||
tax_rule integer The internal ID of the applied tax rule (or ``null``).
|
||||
@@ -48,11 +50,22 @@ sales_channels list of strings Sales channel
|
||||
``"web"`` or ``"resellers"``. Defaults to ``["web"]``.
|
||||
available_from datetime The first date time at which this item can be bought
|
||||
(or ``null``).
|
||||
available_from_mode string If ``hide`` (the default), this item is hidden in the shop
|
||||
if unavailable due to the ``available_from`` setting.
|
||||
If ``info``, the item is visible, but can't be purchased,
|
||||
and a note explaining the unavailability is displayed.
|
||||
available_until datetime The last date time at which this item can be bought
|
||||
(or ``null``).
|
||||
hidden_if_available integer The internal ID of a quota object, or ``null``. If
|
||||
available_until_mode string If ``hide`` (the default), this item is hidden in the shop
|
||||
if unavailable due to the ``available_until`` setting.
|
||||
If ``info``, the item is visible, but can't be purchased,
|
||||
and a note explaining the unavailability is displayed.
|
||||
hidden_if_available integer **DEPRECATED** The internal ID of a quota object, or ``null``. If
|
||||
set, this item won't be shown publicly as long as this
|
||||
quota is available.
|
||||
hidden_if_item_available integer The internal ID of a different item, or ``null``. If
|
||||
set, this item won't be shown publicly as long as this
|
||||
other item is available.
|
||||
require_voucher boolean If ``true``, this item can only be bought using a
|
||||
voucher that is specifically assigned to this item.
|
||||
hide_without_voucher boolean If ``true``, this item is only shown during the voucher
|
||||
@@ -69,6 +82,8 @@ max_per_order integer This product
|
||||
checkin_attention boolean If ``true``, the check-in app should show a warning
|
||||
that this ticket requires special attention if such
|
||||
a product is being scanned.
|
||||
checkin_text string Text that will be shown if a ticket of this type is
|
||||
scanned (or ``null``).
|
||||
original_price money (string) An original price, shown for comparison, not used
|
||||
for price calculations (or ``null``).
|
||||
require_approval boolean If ``true``, orders with this product will need to be
|
||||
@@ -123,6 +138,8 @@ variations list of objects A list with o
|
||||
├ price money (string) The price used for this variation. This is either the
|
||||
same as ``default_price`` if that value is set or equal
|
||||
to the item's ``default_price``.
|
||||
├ free_price_suggestion money (string) A suggested price, used as a default value if
|
||||
``free_price`` is set (or ``null``).
|
||||
├ original_price money (string) An original price, shown for comparison, not used
|
||||
for price calculations (or ``null``).
|
||||
├ active boolean If ``false``, this variation will not be sold or shown.
|
||||
@@ -130,6 +147,8 @@ variations list of objects A list with o
|
||||
├ checkin_attention boolean If ``true``, the check-in app should show a warning
|
||||
that this ticket requires special attention if such
|
||||
a variation is being scanned.
|
||||
├ checkin_text string Text that will be shown if a ticket of this type is
|
||||
scanned (or ``null``).
|
||||
├ require_approval boolean If ``true``, orders with this variation will need to be
|
||||
approved by the event organizer before they can be
|
||||
paid.
|
||||
@@ -145,8 +164,16 @@ variations list of objects A list with o
|
||||
available.
|
||||
├ available_from datetime The first date time at which this variation can be bought
|
||||
(or ``null``).
|
||||
├ available_from_mode string If ``hide`` (the default), this variation is hidden in the shop
|
||||
if unavailable due to the ``available_from`` setting.
|
||||
If ``info``, the variation is visible, but can't be purchased,
|
||||
and a note explaining the unavailability is displayed.
|
||||
├ available_until datetime The last date time at which this variation can be bought
|
||||
(or ``null``).
|
||||
├ available_until_mode string If ``hide`` (the default), this variation is hidden in the shop
|
||||
if unavailable due to the ``available_until`` setting.
|
||||
If ``info``, the variation is visible, but can't be purchased,
|
||||
and a note explaining the unavailability is displayed.
|
||||
├ hide_without_voucher boolean If ``true``, this variation is only shown during the voucher
|
||||
redemption process, but not in the normal shop
|
||||
frontend.
|
||||
@@ -196,6 +223,16 @@ meta_data object Values set fo
|
||||
|
||||
The ``media_policy`` and ``media_type`` attributes have been added.
|
||||
|
||||
.. versionchanged:: 2023.10
|
||||
|
||||
The ``checkin_text`` and ``variations[x].checkin_text`` attributes have been added.
|
||||
The ``free_price_suggestion`` and ``variations[x].free_price_suggestion`` attributes have been added.
|
||||
|
||||
.. versionchanged:: 2023.10
|
||||
|
||||
The ``hidden_if_item_available`` attributes has been added, the ``hidden_if_available`` attribute has been
|
||||
deprecated.
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
@@ -246,6 +283,7 @@ Endpoints
|
||||
"active": true,
|
||||
"description": null,
|
||||
"free_price": false,
|
||||
"free_price_suggestion": null,
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": 1,
|
||||
"admission": false,
|
||||
@@ -257,14 +295,18 @@ Endpoints
|
||||
"position": 0,
|
||||
"picture": null,
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hidden_if_available": null,
|
||||
"hidden_if_item_available": null,
|
||||
"require_voucher": false,
|
||||
"hide_without_voucher": false,
|
||||
"allow_cancel": true,
|
||||
"min_per_order": null,
|
||||
"max_per_order": null,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"has_variations": false,
|
||||
"generate_tickets": null,
|
||||
"allow_waitinglist": true,
|
||||
@@ -291,14 +333,18 @@ Endpoints
|
||||
"default_price": "10.00",
|
||||
"price": "10.00",
|
||||
"original_price": null,
|
||||
"free_price_suggestion": null,
|
||||
"active": true,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"meta_data": {},
|
||||
@@ -309,14 +355,18 @@ Endpoints
|
||||
"default_price": null,
|
||||
"price": "23.00",
|
||||
"original_price": null,
|
||||
"free_price_suggestion": null,
|
||||
"active": true,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"meta_data": {},
|
||||
@@ -377,6 +427,7 @@ Endpoints
|
||||
"active": true,
|
||||
"description": null,
|
||||
"free_price": false,
|
||||
"free_price_suggestion": null,
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": 1,
|
||||
"admission": false,
|
||||
@@ -388,8 +439,11 @@ Endpoints
|
||||
"position": 0,
|
||||
"picture": null,
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hidden_if_available": null,
|
||||
"hidden_if_item_available": null,
|
||||
"require_voucher": false,
|
||||
"hide_without_voucher": false,
|
||||
"allow_cancel": true,
|
||||
@@ -399,6 +453,7 @@ Endpoints
|
||||
"min_per_order": null,
|
||||
"max_per_order": null,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"has_variations": false,
|
||||
"require_approval": false,
|
||||
"require_bundling": false,
|
||||
@@ -422,15 +477,19 @@ Endpoints
|
||||
"default_price": "10.00",
|
||||
"price": "10.00",
|
||||
"original_price": null,
|
||||
"free_price_suggestion": null,
|
||||
"active": true,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"description": null,
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": false,
|
||||
"meta_data": {},
|
||||
"position": 0
|
||||
@@ -440,14 +499,18 @@ Endpoints
|
||||
"default_price": null,
|
||||
"price": "23.00",
|
||||
"original_price": null,
|
||||
"free_price_suggestion": null,
|
||||
"active": true,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"meta_data": {},
|
||||
@@ -489,6 +552,7 @@ Endpoints
|
||||
"active": true,
|
||||
"description": null,
|
||||
"free_price": false,
|
||||
"free_price_suggestion": null,
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": 1,
|
||||
"admission": false,
|
||||
@@ -500,8 +564,11 @@ Endpoints
|
||||
"position": 0,
|
||||
"picture": null,
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hidden_if_available": null,
|
||||
"hidden_if_item_available": null,
|
||||
"require_voucher": false,
|
||||
"hide_without_voucher": false,
|
||||
"allow_cancel": true,
|
||||
@@ -511,6 +578,7 @@ Endpoints
|
||||
"min_per_order": null,
|
||||
"max_per_order": null,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"require_bundling": false,
|
||||
"require_membership": false,
|
||||
@@ -533,14 +601,18 @@ Endpoints
|
||||
"default_price": "10.00",
|
||||
"price": "10.00",
|
||||
"original_price": null,
|
||||
"free_price_suggestion": null,
|
||||
"active": true,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"meta_data": {},
|
||||
@@ -551,14 +623,18 @@ Endpoints
|
||||
"default_price": null,
|
||||
"price": "23.00",
|
||||
"original_price": null,
|
||||
"free_price_suggestion": null,
|
||||
"active": true,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"meta_data": {},
|
||||
@@ -588,6 +664,7 @@ Endpoints
|
||||
"active": true,
|
||||
"description": null,
|
||||
"free_price": false,
|
||||
"free_price_suggestion": null,
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": 1,
|
||||
"admission": false,
|
||||
@@ -599,8 +676,11 @@ Endpoints
|
||||
"position": 0,
|
||||
"picture": null,
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hidden_if_available": null,
|
||||
"hidden_if_item_available": null,
|
||||
"require_voucher": false,
|
||||
"hide_without_voucher": false,
|
||||
"allow_cancel": true,
|
||||
@@ -610,6 +690,7 @@ Endpoints
|
||||
"allow_waitinglist": true,
|
||||
"show_quota_left": null,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"has_variations": true,
|
||||
"require_approval": false,
|
||||
"require_bundling": false,
|
||||
@@ -633,14 +714,18 @@ Endpoints
|
||||
"default_price": "10.00",
|
||||
"price": "10.00",
|
||||
"original_price": null,
|
||||
"free_price_suggestion": null,
|
||||
"active": true,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"meta_data": {},
|
||||
@@ -651,14 +736,18 @@ Endpoints
|
||||
"default_price": null,
|
||||
"price": "23.00",
|
||||
"original_price": null,
|
||||
"free_price_suggestion": null,
|
||||
"active": true,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"meta_data": {},
|
||||
@@ -719,6 +808,7 @@ Endpoints
|
||||
"active": true,
|
||||
"description": null,
|
||||
"free_price": false,
|
||||
"free_price_suggestion": null,
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": 1,
|
||||
"admission": false,
|
||||
@@ -730,8 +820,11 @@ Endpoints
|
||||
"position": 0,
|
||||
"picture": null,
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hidden_if_available": null,
|
||||
"hidden_if_item_available": null,
|
||||
"require_voucher": false,
|
||||
"hide_without_voucher": false,
|
||||
"generate_tickets": null,
|
||||
@@ -741,6 +834,7 @@ Endpoints
|
||||
"min_per_order": null,
|
||||
"max_per_order": null,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"has_variations": true,
|
||||
"require_approval": false,
|
||||
"require_bundling": false,
|
||||
@@ -764,14 +858,18 @@ Endpoints
|
||||
"default_price": "10.00",
|
||||
"price": "10.00",
|
||||
"original_price": null,
|
||||
"free_price_suggestion": null,
|
||||
"active": true,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"meta_data": {},
|
||||
@@ -782,14 +880,18 @@ Endpoints
|
||||
"default_price": null,
|
||||
"price": "23.00",
|
||||
"original_price": null,
|
||||
"free_price_suggestion": null,
|
||||
"active": true,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"meta_data": {},
|
||||
|
||||
@@ -46,6 +46,8 @@ custom_followup_at date Internal date f
|
||||
checkin_attention boolean If ``true``, the check-in app should show a warning
|
||||
that this ticket requires special attention if a ticket
|
||||
of this order is scanned.
|
||||
checkin_text string Text that will be shown if a ticket of this order is
|
||||
scanned (or ``null``).
|
||||
invoice_address object Invoice address information (can be ``null``)
|
||||
├ last_modified datetime Last modification date of the address
|
||||
├ company string Customer company name
|
||||
@@ -135,6 +137,18 @@ last_modified datetime Last modificati
|
||||
|
||||
The ``event`` attribute has been added. The organizer-level endpoint has been added.
|
||||
|
||||
.. versionchanged:: 2023.9
|
||||
|
||||
The ``customer`` query parameter has been added.
|
||||
|
||||
.. versionchanged:: 2023.10
|
||||
|
||||
The ``checkin_text`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 2024.1
|
||||
|
||||
The ``expires`` attribute can now be passed during order creation.
|
||||
|
||||
|
||||
.. _order-position-resource:
|
||||
|
||||
@@ -165,6 +179,11 @@ country string Attendee countr
|
||||
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_budget_use money (string) Amount of money discounted by the voucher, corresponding
|
||||
to how much of the ``budget`` of the voucher is consumed.
|
||||
**Important:** Do not rely on this amount to be a useful
|
||||
value if the position's price, product or voucher
|
||||
are changed *after* the order was created. Can be ``null``.
|
||||
tax_rate decimal (string) VAT rate applied for this position
|
||||
tax_value money (string) VAT included in this position
|
||||
tax_rule integer The ID of the used tax rule (or ``null``)
|
||||
@@ -314,6 +333,7 @@ List of all orders
|
||||
"comment": "",
|
||||
"custom_followup_at": null,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"valid_if_pending": false,
|
||||
"invoice_address": {
|
||||
@@ -352,6 +372,7 @@ List of all orders
|
||||
"country": "DE",
|
||||
"state": null,
|
||||
"voucher": null,
|
||||
"voucher_budget_use": null,
|
||||
"tax_rate": "0.00",
|
||||
"tax_value": "0.00",
|
||||
"tax_rule": null,
|
||||
@@ -420,6 +441,7 @@ List of all orders
|
||||
:query string code: Only return orders that match the given order code
|
||||
:query string status: Only return orders in the given order status (see above)
|
||||
:query string search: Only return orders matching a given search query (matching for names, email addresses, and company names)
|
||||
:query string customer: Only show orders linked to the given customer.
|
||||
:query integer item: Only return orders with a position that contains this item ID. *Warning:* Result will also include orders if they contain mixed items, and it will even return orders where the item is only contained in a canceled position.
|
||||
:query integer variation: Only return orders with a position that contains this variation ID. *Warning:* Result will also include orders if they contain mixed items and variations, and it will even return orders where the variation is only contained in a canceled position.
|
||||
:query boolean testmode: Only return orders with ``testmode`` set to ``true`` or ``false``
|
||||
@@ -534,6 +556,7 @@ Fetching individual orders
|
||||
"comment": "",
|
||||
"custom_followup_at": null,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"valid_if_pending": false,
|
||||
"invoice_address": {
|
||||
@@ -572,6 +595,7 @@ Fetching individual orders
|
||||
"country": "DE",
|
||||
"state": null,
|
||||
"voucher": null,
|
||||
"voucher_budget_use": null,
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": null,
|
||||
"tax_value": "0.00",
|
||||
@@ -704,6 +728,8 @@ Updating order fields
|
||||
|
||||
* ``checkin_attention``
|
||||
|
||||
* ``checkin_text``
|
||||
|
||||
* ``locale``
|
||||
|
||||
* ``comment``
|
||||
@@ -714,6 +740,8 @@ Updating order fields
|
||||
|
||||
* ``valid_if_pending``
|
||||
|
||||
* ``expires``
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
@@ -919,6 +947,7 @@ Creating orders
|
||||
* ``comment`` (optional)
|
||||
* ``custom_followup_at`` (optional)
|
||||
* ``checkin_attention`` (optional)
|
||||
* ``checkin_text`` (optional)
|
||||
* ``require_approval`` (optional)
|
||||
* ``valid_if_pending`` (optional)
|
||||
* ``invoice_address`` (optional)
|
||||
@@ -1519,6 +1548,7 @@ List of all order positions
|
||||
},
|
||||
"attendee_email": null,
|
||||
"voucher": null,
|
||||
"voucher_budget_use": null,
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": null,
|
||||
"tax_value": "0.00",
|
||||
@@ -1566,6 +1596,7 @@ List of all order positions
|
||||
``order__datetime,positionid``
|
||||
:query string order: Only return positions of the order with the given order code
|
||||
:query string search: Fuzzy search matching the attendee name, order code, invoice address name as well as to the beginning of the secret.
|
||||
:query string customer: Only show orders linked to the given customer.
|
||||
:query integer item: Only return positions with the purchased item matching the given ID.
|
||||
:query integer item__in: Only return positions with the purchased item matching one of the given comma-separated IDs.
|
||||
:query integer variation: Only return positions with the purchased item variation matching the given ID.
|
||||
@@ -1631,6 +1662,7 @@ Fetching individual positions
|
||||
},
|
||||
"attendee_email": null,
|
||||
"voucher": null,
|
||||
"voucher_budget_use": null,
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": null,
|
||||
"tax_value": "0.00",
|
||||
|
||||
@@ -44,6 +44,8 @@ identifier string An arbitrary st
|
||||
ask_during_checkin boolean If ``true``, this question will not be asked while
|
||||
buying the ticket, but will show up when redeeming
|
||||
the ticket instead.
|
||||
show_during_checkin boolean If ``true``, the answer to the question will be shown
|
||||
during check-in (if the check-in client supports it).
|
||||
hidden boolean If ``true``, the question will only be shown in the
|
||||
backend.
|
||||
print_on_invoice boolean If ``true``, the question will only be shown on
|
||||
@@ -77,6 +79,10 @@ dependency_value string An old version
|
||||
for one value. **Deprecated.**
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 2023.8
|
||||
|
||||
The ``show_during_checkin`` attribute has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -115,6 +121,7 @@ Endpoints
|
||||
"position": 1,
|
||||
"identifier": "WY3TP9SL",
|
||||
"ask_during_checkin": false,
|
||||
"show_during_checkin": false,
|
||||
"hidden": false,
|
||||
"print_on_invoice": false,
|
||||
"valid_number_min": null,
|
||||
@@ -194,6 +201,7 @@ Endpoints
|
||||
"position": 1,
|
||||
"identifier": "WY3TP9SL",
|
||||
"ask_during_checkin": false,
|
||||
"show_during_checkin": false,
|
||||
"hidden": false,
|
||||
"print_on_invoice": false,
|
||||
"valid_number_min": null,
|
||||
@@ -257,6 +265,7 @@ Endpoints
|
||||
"items": [1, 2],
|
||||
"position": 1,
|
||||
"ask_during_checkin": false,
|
||||
"show_during_checkin": false,
|
||||
"hidden": false,
|
||||
"print_on_invoice": false,
|
||||
"dependency_question": null,
|
||||
@@ -293,6 +302,7 @@ Endpoints
|
||||
"position": 1,
|
||||
"identifier": "WY3TP9SL",
|
||||
"ask_during_checkin": false,
|
||||
"show_during_checkin": false,
|
||||
"hidden": false,
|
||||
"print_on_invoice": false,
|
||||
"dependency_question": null,
|
||||
@@ -348,7 +358,7 @@ Endpoints
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/bigevents/events/sampleconf/items/1/ HTTP/1.1
|
||||
PATCH /api/v1/organizers/bigevents/events/sampleconf/questions/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
@@ -376,6 +386,7 @@ Endpoints
|
||||
"position": 2,
|
||||
"identifier": "WY3TP9SL",
|
||||
"ask_during_checkin": false,
|
||||
"show_during_checkin": false,
|
||||
"hidden": false,
|
||||
"print_on_invoice": false,
|
||||
"dependency_question": null,
|
||||
@@ -415,7 +426,7 @@ Endpoints
|
||||
:param event: The ``slug`` field of the event to modify
|
||||
:param id: The ``id`` field of the question to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The item could not be modified due to invalid submitted data
|
||||
:statuscode 400: The question could not be modified due to invalid submitted data
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
|
||||
|
||||
@@ -427,7 +438,7 @@ Endpoints
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v1/organizers/bigevents/events/sampleconf/items/1/ HTTP/1.1
|
||||
DELETE /api/v1/organizers/bigevents/events/sampleconf/questions/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
@@ -440,7 +451,7 @@ Endpoints
|
||||
|
||||
: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 item to delete
|
||||
:param id: The ``id`` field of the question to delete
|
||||
:statuscode 204: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
|
||||
|
||||
556
doc/api/resources/scheduled_exports.rst
Normal file
556
doc/api/resources/scheduled_exports.rst
Normal file
@@ -0,0 +1,556 @@
|
||||
.. spelling:word-list:: checkin
|
||||
|
||||
Scheduled data exports
|
||||
======================
|
||||
|
||||
pretix and it's plugins include a number of data exporters that allow you to bulk download various data from pretix in
|
||||
different formats. You should read :ref:`rest-exporters` first to get an understanding of the basic mechanism.
|
||||
|
||||
Exports can be scheduled to be sent at specific times automatically, both on organizer level and event level.
|
||||
|
||||
Scheduled export resource
|
||||
-------------------------
|
||||
|
||||
The scheduled export contains the following public fields:
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
===================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal ID of the schedule
|
||||
owner string Email address of the user who created this schedule (read-only).
|
||||
This address will always receive the export and the export
|
||||
will only contain data that this user has permission
|
||||
to access at the time of the export. **We consider this
|
||||
field experimental, it's behaviour might change in the future.
|
||||
Note that the email address of a user can change at any time.**
|
||||
export_identifier string Identifier of the export to run, see :ref:`rest-exporters`
|
||||
export_form_data object Input data for the export, format depends on the export,
|
||||
see :ref:`rest-exporters` for more details.
|
||||
locale string Language to run the export in
|
||||
mail_additional_recipients string Email addresses to receive the export, comma-separated (or empty string)
|
||||
mail_additional_recipients_cc string Email addresses to receive the export in copy, comma-separated (or empty string)
|
||||
mail_additional_recipients_bcc string Email addresses to receive the exportin blind copy, comma-separated (or empty string)
|
||||
mail_subject string Subject to use for the email (currently no variables supported)
|
||||
mail_template string Text to use for the email (currently no variables supported)
|
||||
schedule_rrule string Recurrence specification to determine the **days** this
|
||||
schedule runs on in ``RRULE`` syntax following `RFC 5545`_
|
||||
with some restrictions. Only one rule is allowed, only
|
||||
one occurrence per day is allowed, and some features
|
||||
are not supported (``BYMONTHDAY``, ``BYYEARDAY``,
|
||||
``BYEASTER``, ``BYWEEKNO``).
|
||||
schedule_rrule_time time Time of day to run this on on the specified days.
|
||||
Will be interpreted as local time of the event for event-level
|
||||
exports. For organizer-level exports, the timezone is given
|
||||
in the field ``timezone``. The export will never run **before**
|
||||
this time but it **may** run **later**.
|
||||
timezone string Time zone to interpret the schedule in (only for organizer-level exports)
|
||||
schedule_next_run datetime Next planned execution (read-only, computed by server)
|
||||
error_counter integer Number of consecutive times this export failed (read-only).
|
||||
After a number of failures (currently 5), the schedule no
|
||||
longer is executed. Changing parameters resets the value.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
Special notes on permissions
|
||||
----------------------------
|
||||
|
||||
Permission handling for scheduled exports is more complex than for most other objects. The reason for this is that
|
||||
there are two levels of access control involved here: First, you need permission to access or change the configuration
|
||||
of the scheduled exports in the moment you are doing it. Second, you **continuously** need permission to access the
|
||||
**data** that is exported as part of the schedule. For this reason, scheduled exports always need one user account
|
||||
to be their **owner**.
|
||||
|
||||
Therefore, scheduled exports **must** be created by an API client using :ref:`OAuth authentication <rest-oauth>`.
|
||||
It is impossible to create a scheduled export using token authentication. After the export is created, it can also be
|
||||
modified using token authentication.
|
||||
|
||||
A user or token with the "can change settings" permission for a given organizer or event can see and change
|
||||
**all** scheduled exports created for the respective organizer or event, regardless of who created them.
|
||||
A user without this permission can only see **their own** scheduled exports.
|
||||
A token without this permission can not see scheduled exports as all.
|
||||
|
||||
|
||||
|
||||
Endpoints for event exports
|
||||
---------------------------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/scheduled_exports/
|
||||
|
||||
Returns a list of all scheduled exports the client has access to.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/scheduled_exports/ 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,
|
||||
"owner": "john@example.com",
|
||||
"export_identifier": "orderlist",
|
||||
"export_form_data": {"_format": "xlsx", "date_range": "week_previous"},
|
||||
"locale": "en",
|
||||
"mail_additional_recipients": "mary@example.org",
|
||||
"mail_additional_recipients_cc": "",
|
||||
"mail_additional_recipients_bcc": "",
|
||||
"mail_subject": "Order list",
|
||||
"mail_template": "Here is last week's order list\n\nCheers\nJohn",
|
||||
"schedule_rrule": "DTSTART:20230118T000000\nRRULE:FREQ=WEEKLY;BYDAY=TU,WE,TH",
|
||||
"schedule_rrule_time": "04:00:00",
|
||||
"schedule_next_run": "2023-10-26T02:00:00Z",
|
||||
"error_counter": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``id``, ``export_identifier``, and ``schedule_next_run``.
|
||||
Default: ``id``
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/scheduled_exports/(id)/
|
||||
|
||||
Returns information on one scheduled export, identified by its ID.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/scheduled_exports/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,
|
||||
"owner": "john@example.com",
|
||||
"export_identifier": "orderlist",
|
||||
"export_form_data": {"_format": "xlsx", "date_range": "week_previous"},
|
||||
"locale": "en",
|
||||
"mail_additional_recipients": "mary@example.org",
|
||||
"mail_additional_recipients_cc": "",
|
||||
"mail_additional_recipients_bcc": "",
|
||||
"mail_subject": "Order list",
|
||||
"mail_template": "Here is last week's order list\n\nCheers\nJohn",
|
||||
"schedule_rrule": "DTSTART:20230118T000000\nRRULE:FREQ=WEEKLY;BYDAY=TU,WE,TH",
|
||||
"schedule_rrule_time": "04:00:00",
|
||||
"schedule_next_run": "2023-10-26T02:00:00Z",
|
||||
"error_counter": 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 scheduled export to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/scheduled_exports/
|
||||
|
||||
Schedule a new export.
|
||||
|
||||
.. note:: See above for special notes on permissions.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/scheduled_exports/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"export_identifier": "orderlist",
|
||||
"export_form_data": {"_format": "xlsx", "date_range": "week_previous"},
|
||||
"locale": "en",
|
||||
"mail_additional_recipients": "mary@example.org",
|
||||
"mail_additional_recipients_cc": "",
|
||||
"mail_additional_recipients_bcc": "",
|
||||
"mail_subject": "Order list",
|
||||
"mail_template": "Here is last week's order list\n\nCheers\nJohn",
|
||||
"schedule_rrule": "DTSTART:20230118T000000\nRRULE:FREQ=WEEKLY;BYDAY=TU,WE,TH",
|
||||
"schedule_rrule_time": "04:00:00"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"owner": "john@example.com",
|
||||
"export_identifier": "orderlist",
|
||||
"export_form_data": {"_format": "xlsx", "date_range": "week_previous"},
|
||||
"locale": "en",
|
||||
"mail_additional_recipients": "mary@example.org",
|
||||
"mail_additional_recipients_cc": "",
|
||||
"mail_additional_recipients_bcc": "",
|
||||
"mail_subject": "Order list",
|
||||
"mail_template": "Here is last week's order list\n\nCheers\nJohn",
|
||||
"schedule_rrule": "DTSTART:20230118T000000\nRRULE:FREQ=WEEKLY;BYDAY=TU,WE,TH",
|
||||
"schedule_rrule_time": "04:00:00",
|
||||
"schedule_next_run": "2023-10-26T02:00:00Z",
|
||||
"error_counter": 0
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer of the event to create an item for
|
||||
:param event: The ``slug`` field of the event to create an item for
|
||||
:statuscode 201: no error
|
||||
:statuscode 400: The item could not be created due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
|
||||
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/scheduled_exports/(id)/
|
||||
|
||||
Update a scheduled export. 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/scheduled_exports/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 94
|
||||
|
||||
{
|
||||
"export_form_data": {"_format": "xlsx", "date_range": "week_this"},
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"owner": "john@example.com",
|
||||
"export_identifier": "orderlist",
|
||||
"export_form_data": {"_format": "xlsx", "date_range": "week_this"},
|
||||
"locale": "en",
|
||||
"mail_additional_recipients": "mary@example.org",
|
||||
"mail_additional_recipients_cc": "",
|
||||
"mail_additional_recipients_bcc": "",
|
||||
"mail_subject": "Order list",
|
||||
"mail_template": "Here is last week's order list\n\nCheers\nJohn",
|
||||
"schedule_rrule": "DTSTART:20230118T000000\nRRULE:FREQ=WEEKLY;BYDAY=TU,WE,TH",
|
||||
"schedule_rrule_time": "04:00:00",
|
||||
"schedule_next_run": "2023-10-26T02:00:00Z",
|
||||
"error_counter": 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 export to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The export could not be modified due to invalid submitted data
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
|
||||
|
||||
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/scheduled_exports/(id)/
|
||||
|
||||
Delete a scheduled export.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v1/organizers/bigevents/events/sampleconf/scheduled_exports/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 export to delete
|
||||
:statuscode 204: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
|
||||
|
||||
Endpoints for organizer exports
|
||||
---------------------------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/scheduled_exports/
|
||||
|
||||
Returns a list of all scheduled exports the client has access to.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/scheduled_exports/ 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,
|
||||
"owner": "john@example.com",
|
||||
"export_identifier": "orderlist",
|
||||
"export_form_data": {"_format": "xlsx", "date_range": "week_previous"},
|
||||
"locale": "en",
|
||||
"mail_additional_recipients": "mary@example.org",
|
||||
"mail_additional_recipients_cc": "",
|
||||
"mail_additional_recipients_bcc": "",
|
||||
"mail_subject": "Order list",
|
||||
"mail_template": "Here is last week's order list\n\nCheers\nJohn",
|
||||
"schedule_rrule": "DTSTART:20230118T000000\nRRULE:FREQ=WEEKLY;BYDAY=TU,WE,TH",
|
||||
"schedule_rrule_time": "04:00:00",
|
||||
"schedule_next_run": "2023-10-26T02:00:00Z",
|
||||
"timezone": "Europe/Berlin",
|
||||
"error_counter": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``id``, ``export_identifier``, and ``schedule_next_run``.
|
||||
Default: ``id``
|
||||
: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)/scheduled_exports/(id)/
|
||||
|
||||
Returns information on one scheduled export, identified by its ID.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/scheduled_exports/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,
|
||||
"owner": "john@example.com",
|
||||
"export_identifier": "orderlist",
|
||||
"export_form_data": {"_format": "xlsx", "date_range": "week_previous"},
|
||||
"locale": "en",
|
||||
"mail_additional_recipients": "mary@example.org",
|
||||
"mail_additional_recipients_cc": "",
|
||||
"mail_additional_recipients_bcc": "",
|
||||
"mail_subject": "Order list",
|
||||
"mail_template": "Here is last week's order list\n\nCheers\nJohn",
|
||||
"schedule_rrule": "DTSTART:20230118T000000\nRRULE:FREQ=WEEKLY;BYDAY=TU,WE,TH",
|
||||
"schedule_rrule_time": "04:00:00",
|
||||
"schedule_next_run": "2023-10-26T02:00:00Z",
|
||||
"timezone": "Europe/Berlin",
|
||||
"error_counter": 0
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param id: The ``id`` field of the scheduled export 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)/scheduled_exports/
|
||||
|
||||
Schedule a new export.
|
||||
|
||||
.. note:: See above for special notes on permissions.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/scheduled_exports/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"export_identifier": "orderlist",
|
||||
"export_form_data": {"_format": "xlsx", "date_range": "week_previous"},
|
||||
"locale": "en",
|
||||
"mail_additional_recipients": "mary@example.org",
|
||||
"mail_additional_recipients_cc": "",
|
||||
"mail_additional_recipients_bcc": "",
|
||||
"mail_subject": "Order list",
|
||||
"mail_template": "Here is last week's order list\n\nCheers\nJohn",
|
||||
"schedule_rrule": "DTSTART:20230118T000000\nRRULE:FREQ=WEEKLY;BYDAY=TU,WE,TH",
|
||||
"schedule_rrule_time": "04:00:00",
|
||||
"timezone": "Europe/Berlin"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"owner": "john@example.com",
|
||||
"export_identifier": "orderlist",
|
||||
"export_form_data": {"_format": "xlsx", "date_range": "week_previous"},
|
||||
"locale": "en",
|
||||
"mail_additional_recipients": "mary@example.org",
|
||||
"mail_additional_recipients_cc": "",
|
||||
"mail_additional_recipients_bcc": "",
|
||||
"mail_subject": "Order list",
|
||||
"mail_template": "Here is last week's order list\n\nCheers\nJohn",
|
||||
"schedule_rrule": "DTSTART:20230118T000000\nRRULE:FREQ=WEEKLY;BYDAY=TU,WE,TH",
|
||||
"schedule_rrule_time": "04:00:00",
|
||||
"schedule_next_run": "2023-10-26T02:00:00Z",
|
||||
"timezone": "Europe/Berlin",
|
||||
"error_counter": 0
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer of the event to create an item for
|
||||
:statuscode 201: no error
|
||||
:statuscode 400: The item could not be created due to invalid submitted data.
|
||||
: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)/scheduled_exports/(id)/
|
||||
|
||||
Update a scheduled export. 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/scheduled_exports/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 94
|
||||
|
||||
{
|
||||
"export_form_data": {"_format": "xlsx", "date_range": "week_this"},
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"owner": "john@example.com",
|
||||
"export_identifier": "orderlist",
|
||||
"export_form_data": {"_format": "xlsx", "date_range": "week_this"},
|
||||
"locale": "en",
|
||||
"mail_additional_recipients": "mary@example.org",
|
||||
"mail_additional_recipients_cc": "",
|
||||
"mail_additional_recipients_bcc": "",
|
||||
"mail_subject": "Order list",
|
||||
"mail_template": "Here is last week's order list\n\nCheers\nJohn",
|
||||
"schedule_rrule": "DTSTART:20230118T000000\nRRULE:FREQ=WEEKLY;BYDAY=TU,WE,TH",
|
||||
"schedule_rrule_time": "04:00:00",
|
||||
"schedule_next_run": "2023-10-26T02:00:00Z",
|
||||
"timezone": "Europe/Berlin",
|
||||
"error_counter": 0
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param id: The ``id`` field of the export to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The export 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)/scheduled_exports/(id)/
|
||||
|
||||
Delete a scheduled export.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v1/organizers/bigevents/scheduled_exports/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 id: The ``id`` field of the export to delete
|
||||
:statuscode 204: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to delete this resource.
|
||||
|
||||
|
||||
.. _RFC 5545: https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.5.3
|
||||
@@ -22,6 +22,8 @@ id integer Internal ID of
|
||||
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
|
||||
require_2fa boolean Whether members of this team are required to use
|
||||
two-factor authentication
|
||||
can_create_events boolean
|
||||
can_change_teams boolean
|
||||
can_change_organizer_settings boolean
|
||||
@@ -122,6 +124,7 @@ Team endpoints
|
||||
"name": "Admin team",
|
||||
"all_events": true,
|
||||
"limit_events": [],
|
||||
"require_2fa": true,
|
||||
"can_create_events": true,
|
||||
...
|
||||
}
|
||||
@@ -159,6 +162,7 @@ Team endpoints
|
||||
"name": "Admin team",
|
||||
"all_events": true,
|
||||
"limit_events": [],
|
||||
"require_2fa": true,
|
||||
"can_create_events": true,
|
||||
...
|
||||
}
|
||||
@@ -186,6 +190,7 @@ Team endpoints
|
||||
"name": "Admin team",
|
||||
"all_events": true,
|
||||
"limit_events": [],
|
||||
"require_2fa": true,
|
||||
"can_create_events": true,
|
||||
...
|
||||
}
|
||||
@@ -203,6 +208,7 @@ Team endpoints
|
||||
"name": "Admin team",
|
||||
"all_events": true,
|
||||
"limit_events": [],
|
||||
"require_2fa": true,
|
||||
"can_create_events": true,
|
||||
...
|
||||
}
|
||||
@@ -246,6 +252,7 @@ Team endpoints
|
||||
"name": "Admin team",
|
||||
"all_events": true,
|
||||
"limit_events": [],
|
||||
"require_2fa": true,
|
||||
"can_create_events": true,
|
||||
...
|
||||
}
|
||||
|
||||
@@ -11,9 +11,10 @@ Core
|
||||
----
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types,
|
||||
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types, notification,
|
||||
item_copy_data, register_sales_channels, register_global_settings, quota_availability, global_email_filter,
|
||||
register_ticket_secret_generators, gift_card_transaction_display
|
||||
register_ticket_secret_generators, gift_card_transaction_display,
|
||||
register_text_placeholders, register_mail_placeholders
|
||||
|
||||
Order events
|
||||
""""""""""""
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
|
||||
.. _`importcol`:
|
||||
|
||||
Extending the order import process
|
||||
==================================
|
||||
Extending the import process
|
||||
============================
|
||||
|
||||
It's possible through the backend to import orders into pretix, for example from a legacy ticketing system. If your
|
||||
plugins defines additional data structures around orders, it might be useful to make it possible to import them as well.
|
||||
It's possible through the backend to import objects into pretix, for example orders from a legacy ticketing system. If
|
||||
your plugin defines additional data structures around those objects, it might be useful to make it possible to import
|
||||
them as well.
|
||||
|
||||
Import process
|
||||
--------------
|
||||
@@ -40,7 +41,7 @@ Column registration
|
||||
|
||||
The import API does not make a lot of usage from signals, however, it
|
||||
does use a signal to get a list of all available import columns. Your plugin
|
||||
should listen for this signal and return the subclass of ``pretix.base.orderimport.ImportColumn``
|
||||
should listen for this signal and return the subclass of ``pretix.base.modelimport.ImportColumn``
|
||||
that we'll provide in this plugin:
|
||||
|
||||
.. sourcecode:: python
|
||||
@@ -56,10 +57,16 @@ that we'll provide in this plugin:
|
||||
EmailColumn(sender),
|
||||
]
|
||||
|
||||
Similar signals exist for other objects:
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:members: voucher_import_columns
|
||||
|
||||
|
||||
The column class API
|
||||
--------------------
|
||||
|
||||
.. class:: pretix.base.orderimport.ImportColumn
|
||||
.. class:: pretix.base.modelimport.ImportColumn
|
||||
|
||||
The central object of each import extension is the subclass of ``ImportColumn``.
|
||||
|
||||
|
||||
@@ -84,6 +84,8 @@ convenient to you:
|
||||
|
||||
.. automethod:: _register_fonts
|
||||
|
||||
.. automethod:: _register_event_fonts
|
||||
|
||||
.. automethod:: _on_first_page
|
||||
|
||||
.. automethod:: _on_other_page
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
.. highlight:: python
|
||||
:linenothreshold: 5
|
||||
|
||||
Writing an e-mail placeholder plugin
|
||||
====================================
|
||||
Writing a template placeholder plugin
|
||||
=====================================
|
||||
|
||||
An email placeholder is a dynamic value that pretix users can use in their email templates.
|
||||
A template placeholder is a dynamic value that pretix users can use in their email templates and in other
|
||||
configurable texts.
|
||||
|
||||
Please read :ref:`Creating a plugin <pluginsetup>` first, if you haven't already.
|
||||
|
||||
@@ -12,31 +13,31 @@ Placeholder registration
|
||||
------------------------
|
||||
|
||||
The placeholder API does not make a lot of usage from signals, however, it
|
||||
does use a signal to get a list of all available email placeholders. Your plugin
|
||||
should listen for this signal and return an instance of a subclass of ``pretix.base.email.BaseMailTextPlaceholder``:
|
||||
does use a signal to get a list of all available placeholders. Your plugin
|
||||
should listen for this signal and return an instance of a subclass of ``pretix.base.services.placeholders.BaseTextPlaceholder``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.dispatch import receiver
|
||||
|
||||
from pretix.base.signals import register_mail_placeholders
|
||||
from pretix.base.signals import register_text_placeholders
|
||||
|
||||
|
||||
@receiver(register_mail_placeholders, dispatch_uid="placeholder_custom")
|
||||
def register_mail_renderers(sender, **kwargs):
|
||||
from .email import MyPlaceholderClass
|
||||
@receiver(register_text_placeholders, dispatch_uid="placeholder_custom")
|
||||
def register_placeholder_renderers(sender, **kwargs):
|
||||
from .placeholders import MyPlaceholderClass
|
||||
return MyPlaceholder()
|
||||
|
||||
|
||||
Context mechanism
|
||||
-----------------
|
||||
|
||||
Emails are sent in different "contexts" within pretix. For example, many emails are sent in the
|
||||
the context of an order, but some are not, such as the notification of a waiting list voucher.
|
||||
Templates are used in different "contexts" within pretix. For example, many emails are rendered from
|
||||
templates in the context of an order, but some are not, such as the notification of a waiting list voucher.
|
||||
|
||||
Not all placeholders make sense in every email, and placeholders usually depend some parameters
|
||||
Not all placeholders make sense everywhere, and placeholders usually depend on some parameters
|
||||
themselves, such as the ``Order`` object. Therefore, placeholders are expected to explicitly declare
|
||||
what values they depend on and they will only be available in an email if all those dependencies are
|
||||
what values they depend on and they will only be available in a context where all those dependencies are
|
||||
met. Currently, placeholders can depend on the following context parameters:
|
||||
|
||||
* ``event``
|
||||
@@ -51,7 +52,7 @@ There are a few more that are only to be used internally but not by plugins.
|
||||
The placeholder class
|
||||
---------------------
|
||||
|
||||
.. class:: pretix.base.email.BaseMailTextPlaceholder
|
||||
.. class:: pretix.base.services.placeholders.BaseTextPlaceholder
|
||||
|
||||
.. autoattribute:: identifier
|
||||
|
||||
@@ -77,7 +78,15 @@ functions:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
placeholder = SimpleFunctionalMailTextPlaceholder(
|
||||
placeholder = SimpleFunctionalTextPlaceholder(
|
||||
'code', ['order'], lambda order: order.code, sample='F8VVL'
|
||||
)
|
||||
|
||||
Signals
|
||||
-------
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:members: register_text_placeholders
|
||||
.. automodule:: pretix.base.signals
|
||||
:members: register_mail_placeholders
|
||||
|
||||
|
||||
@@ -19,3 +19,4 @@ Contents:
|
||||
permissions
|
||||
logging
|
||||
locking
|
||||
timemachine
|
||||
|
||||
32
doc/development/implementation/timemachine.rst
Normal file
32
doc/development/implementation/timemachine.rst
Normal file
@@ -0,0 +1,32 @@
|
||||
Time machine mode
|
||||
=================
|
||||
|
||||
In test mode, pretix provides a "time machine" feature which allows event organizers
|
||||
to test their shop as if it were a different date and time. To enable this feature, they can
|
||||
click on the "time machine"-link in the test mode warning box on the event page.
|
||||
|
||||
Internally, this time machine mode is implemented by calling our custom :py:meth:`time_machine_now()`
|
||||
function instead of :py:meth:`django.utils.timezone.now()` in all places where the fake time should be
|
||||
taken into account. If you add code that uses the current date and time for checking whether some
|
||||
product can be bought, you should use :py:meth:`time_machine_now`.
|
||||
|
||||
.. autofunction:: pretix.base.timemachine.time_machine_now
|
||||
|
||||
Background tasks
|
||||
----------------
|
||||
|
||||
The time machine datetime is passed through the request flow via a thread-local variable (ContextVar).
|
||||
Therefore, if you call a background task in the order process, where time_machine_now should be
|
||||
respected, you need to pass it through manually as shown in the example below:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@app.task()
|
||||
def my_task(self, override_now_dt: datetime=None) -> None:
|
||||
with time_machine_now_assigned(override_now_dt):
|
||||
# ...do something that uses time_machine_now()
|
||||
|
||||
my_task.apply_async(kwargs={'override_now_dt': time_machine_now(default=None)})
|
||||
|
||||
|
||||
.. autofunction:: pretix.base.timemachine.time_machine_now_assigned
|
||||
@@ -90,6 +90,10 @@ as its first argument and can be used like this::
|
||||
<a href="{% eventurl request.event "presale:event.checkout" step="payment" %}">Pay</a>
|
||||
<a href="{% abseventurl request.event "presale:event.checkout" step="payment" %}">Pay</a>
|
||||
|
||||
To generate absolute URLs on the main domain, you can use the ``absurl`` template tag::
|
||||
|
||||
{% load eventurl %}
|
||||
<a href="{% absmainurl "control:event.settings" organizer=request.event.organizer.slug event=request.event.slug %}">Event settings</a>
|
||||
|
||||
Implementation details
|
||||
----------------------
|
||||
|
||||
@@ -211,5 +211,15 @@ with the documentation a lot, you might find it useful to use sphinx-autobuild::
|
||||
Then, go to http://localhost:8081 for a version of the documentation that automatically re-builds
|
||||
whenever you change a source file.
|
||||
|
||||
Working with frontend assets
|
||||
----------------------------
|
||||
|
||||
To update the frontend styles of shops with a custom styling, run the following commands inside
|
||||
your virtual environment.::
|
||||
|
||||
python -m pretix collectstatic --noinput
|
||||
python -m pretix updatestyles
|
||||
|
||||
|
||||
.. _Django's documentation: https://docs.djangoproject.com/en/1.11/ref/django-admin/#runserver
|
||||
.. _pretixdroid: https://github.com/pretix/pretixdroid
|
||||
|
||||
@@ -31,7 +31,7 @@ pretix/
|
||||
Additional code implementing our customized :ref:`URL handling <urlconf>`.
|
||||
|
||||
static/
|
||||
Contains all static files (CSS/SASS, JavaScript, images) of pretix' core
|
||||
Contains all static files (CSS/SASS, JavaScript, images) of pretix' core.
|
||||
We use libsass as a preprocessor for CSS. Our own sass code is built in the same
|
||||
step as Bootstrap and FontAwesome, so their mixins etc. are fully available.
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 278 KiB After Width: | Height: | Size: 287 KiB |
@@ -25,27 +25,27 @@ partition "data-based check" {
|
||||
else
|
||||
-down->[yes] "Is one or more block set on the ticket?"
|
||||
--> if "" then
|
||||
-right->[no] "Return error BLOCKED"
|
||||
-right->[yes] "Return error BLOCKED"
|
||||
else
|
||||
-down->[yes] "If this is not an exit, is the valid_from/valid_until\nconstraint on the ticket fulfilled?"
|
||||
-down->[no] "Is the order in status PENDING and not yet approved?"
|
||||
--> if "" then
|
||||
-right->[no] "Return error INVALID_TIME"
|
||||
-right->[yes] "Return error UNAPPROVED"
|
||||
else
|
||||
-down->[yes] "Is the product part of the check-in list?"
|
||||
-down->[no] "If this is not an exit, is the valid_from/valid_until\nconstraint on the ticket fulfilled?"
|
||||
--> if "" then
|
||||
-right->[no] "Return error PRODUCT"
|
||||
-right->[no] "Return error INVALID_TIME"
|
||||
else
|
||||
-down->[yes] "Is the subevent part of the check-in list?"
|
||||
-down->[yes] "Is the product part of the check-in list?"
|
||||
--> if "" then
|
||||
-right->[no] "Return error INVALID"
|
||||
note bottom: TODO\ninconsistent\nwith online\ncheck
|
||||
-right->[no] "Return error PRODUCT"
|
||||
else
|
||||
-down->[yes] "Is the order in status PAID?"
|
||||
-down->[yes] "Is the subevent part of the check-in list?"
|
||||
--> if "" then
|
||||
-right->[no] "Is Order.require_approval set?"
|
||||
-right->[no] "Return error INVALID"
|
||||
note bottom: TODO\ninconsistent\nwith online\ncheck
|
||||
else
|
||||
-down->[yes] "Is the order in status PAID?"
|
||||
--> if "" then
|
||||
-->[yes] "Return error UNPAID "
|
||||
else
|
||||
-right->[no] "Is Order.valid_if_pending set?"
|
||||
--> if "" then
|
||||
-->[yes] "Is this an entry or exit?"
|
||||
@@ -62,9 +62,9 @@ partition "data-based check" {
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
else
|
||||
-down->[yes] "Is this an entry or exit?"
|
||||
endif
|
||||
else
|
||||
-down->[yes] "Is this an entry or exit?"
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 180 KiB After Width: | Height: | Size: 183 KiB |
@@ -42,23 +42,25 @@ endif
|
||||
else
|
||||
-down->[yes || force] "Is one or more block set on the ticket?"
|
||||
--> if "" then
|
||||
-right->[no && !force] "Return error BLOCKED"
|
||||
-right->[yes && !force] "Return error BLOCKED"
|
||||
else
|
||||
-down->[yes || force] "If this is not an exit, is the valid_from/valid_until\nconstraint on the ticket fulfilled?"
|
||||
-down->[no || force] "Is the order in status PENDING and not yet approved?"
|
||||
--> if "" then
|
||||
-right->[no && !force] "Return error INVALID_TIME"
|
||||
-right->[yes && !force] "Return error UNAPPROVED"
|
||||
else
|
||||
-down->[yes || force] "Is the product part of the check-in list?"
|
||||
-down->[no || force] "If this is not an exit, is the valid_from/valid_until\nconstraint on the ticket fulfilled?"
|
||||
--> if "" then
|
||||
-right->[no && !force] "Return error PRODUCT"
|
||||
-right->[no && !force] "Return error INVALID_TIME"
|
||||
else
|
||||
-down->[yes || force] "Is the subevent part of the check-in list?"
|
||||
-down->[yes || force] "Is the product part of the check-in list?"
|
||||
--> if "" then
|
||||
-right->[no && !force] "Return error PRODUCT "
|
||||
-right->[no && !force] "Return error PRODUCT"
|
||||
else
|
||||
-down->[yes] "Is the order in status PAID?"
|
||||
-down->[yes || force] "Is the subevent part of the check-in list?"
|
||||
--> if "" then
|
||||
-right->[no && !force] "Is Order.require_approval set?"
|
||||
-right->[no && !force] "Return error PRODUCT "
|
||||
else
|
||||
-down->[yes] "Is the order in status PAID?"
|
||||
--> if "" then
|
||||
-->[no] "Is Order.valid_if_pending set?"
|
||||
--> if "" then
|
||||
@@ -77,10 +79,8 @@ else
|
||||
endif
|
||||
endif
|
||||
else
|
||||
-->[yes] "Return error UNPAID "
|
||||
-down->[yes || force] "Is this an entry or exit?\nIs the upload forced?"
|
||||
endif
|
||||
else
|
||||
-down->[yes || force] "Is this an entry or exit?\nIs the upload forced?"
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
@@ -32,6 +32,7 @@ transactions list of objects Transactions in
|
||||
├ checksum string Checksum computed from payer, reference, amount and
|
||||
date
|
||||
├ payer string Payment source
|
||||
├ external_id string Unique ID of the payment from an external source
|
||||
├ reference string Payment reference
|
||||
├ amount string Payment amount
|
||||
├ iban string Payment IBAN
|
||||
@@ -85,6 +86,7 @@ Endpoints
|
||||
"date": "26.06.2017",
|
||||
"payer": "John Doe",
|
||||
"order": null,
|
||||
"external_id": null,
|
||||
"iban": "",
|
||||
"bic": "",
|
||||
"checksum": "5de03a601644dfa63420dacfd285565f8375a8f2",
|
||||
@@ -139,6 +141,7 @@ Endpoints
|
||||
"iban": "",
|
||||
"bic": "",
|
||||
"order": null,
|
||||
"external_id": null,
|
||||
"checksum": "5de03a601644dfa63420dacfd285565f8375a8f2",
|
||||
"reference": "GUTSCHRIFT\r\nSAMPLECONF-NAB12 EREF: SAMPLECONF-NAB12\r\nIBAN: DE1234556…",
|
||||
"state": "nomatch",
|
||||
|
||||
@@ -34,12 +34,16 @@ internal_id string Can be used for
|
||||
contact_name string Contact person (or ``null``)
|
||||
contact_name_parts object of strings Decomposition of contact name (i.e. given name, family name)
|
||||
contact_email string Contact person email address (or ``null``)
|
||||
contact_cc_email string Copy email addresses, can be multiple separated by comma (or ``null``)
|
||||
booth string Booth number (or ``null``). Maximum 100 characters.
|
||||
locale string Locale for communication with the exhibitor.
|
||||
access_code string Access code for the exhibitor to access their data or use the lead scanning app (read-only).
|
||||
lead_scanning_access_code string Access code for the exhibitor to use the lead scanning app but not access data (read-only).
|
||||
allow_lead_scanning boolean Enables lead scanning app
|
||||
allow_lead_access boolean Enables access to data gathered by the lead scanning app
|
||||
allow_voucher_access boolean Enables access to data gathered by exhibitor vouchers
|
||||
lead_scanning_scope_by_device string Enables lead scanning to be handled as one lead per attendee
|
||||
per scanning device, instead of only per exhibitor.
|
||||
comment string Internal comment, not shown to exhibitor
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
@@ -62,6 +66,7 @@ data list of objects Attendee data s
|
||||
except in a few cases where it contains an additional list of objects
|
||||
with ``value`` and ``label`` keys (e.g. splitting of names).
|
||||
device_name string User-defined name for the device used for scanning (or ``null``).
|
||||
device_uuid string UUID of device used for scanning (or ``null``).
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
Endpoints
|
||||
@@ -105,9 +110,12 @@ Endpoints
|
||||
"title": "Dr"
|
||||
},
|
||||
"contact_email": "johnson@as.example.org",
|
||||
"contact_cc_email": "miller@as.example.org,smith@as.example.org",
|
||||
"booth": "A2",
|
||||
"locale": "de",
|
||||
"access_code": "VKHZ2FU8",
|
||||
"access_code": "VKHZ2FU84",
|
||||
"lead_scanning_access_code": "WVK2B8PZ",
|
||||
"lead_scanning_scope_by_device": false,
|
||||
"allow_lead_scanning": true,
|
||||
"allow_lead_access": true,
|
||||
"allow_voucher_access": true,
|
||||
@@ -156,9 +164,12 @@ Endpoints
|
||||
"title": "Dr"
|
||||
},
|
||||
"contact_email": "johnson@as.example.org",
|
||||
"contact_cc_email": "miller@as.example.org,smith@as.example.org",
|
||||
"booth": "A2",
|
||||
"locale": "de",
|
||||
"access_code": "VKHZ2FU8",
|
||||
"access_code": "VKHZ2FU84",
|
||||
"lead_scanning_access_code": "WVK2B8PZ",
|
||||
"lead_scanning_scope_by_device": false,
|
||||
"allow_lead_scanning": true,
|
||||
"allow_lead_access": true,
|
||||
"allow_voucher_access": true,
|
||||
@@ -357,6 +368,7 @@ Endpoints
|
||||
"title": "Dr"
|
||||
},
|
||||
"contact_email": "johnson@as.example.org",
|
||||
"contact_cc_email": "miller@as.example.org,smith@as.example.org",
|
||||
"booth": "A2",
|
||||
"locale": "de",
|
||||
"allow_lead_scanning": true,
|
||||
@@ -386,9 +398,12 @@ Endpoints
|
||||
"title": "Dr"
|
||||
},
|
||||
"contact_email": "johnson@as.example.org",
|
||||
"contact_cc_email": "miller@as.example.org,smith@as.example.org",
|
||||
"booth": "A2",
|
||||
"locale": "de",
|
||||
"access_code": "VKHZ2FU8",
|
||||
"access_code": "VKHZ2FU84",
|
||||
"lead_scanning_access_code": "WVK2B8PZ",
|
||||
"lead_scanning_scope_by_device": false,
|
||||
"allow_lead_scanning": true,
|
||||
"allow_lead_access": true,
|
||||
"allow_voucher_access": true,
|
||||
@@ -444,9 +459,12 @@ Endpoints
|
||||
"title": "Dr"
|
||||
},
|
||||
"contact_email": "johnson@as.example.org",
|
||||
"contact_cc_email": "miller@as.example.org,smith@as.example.org",
|
||||
"booth": "A2",
|
||||
"locale": "de",
|
||||
"access_code": "VKHZ2FU8",
|
||||
"access_code": "VKHZ2FU84",
|
||||
"lead_scanning_access_code": "WVK2B8PZ",
|
||||
"lead_scanning_scope_by_device": false,
|
||||
"allow_lead_scanning": true,
|
||||
"allow_lead_access": true,
|
||||
"allow_voucher_access": true,
|
||||
@@ -561,6 +579,7 @@ name string Exhibitor name
|
||||
booth string Booth number (or ``null``)
|
||||
event object Object describing the event
|
||||
├ name multi-lingual string Event name
|
||||
├ end_date datetime End date of the event. After this time, the app could show a warning that the event is over.
|
||||
├ imprint_url string URL to legal notice page. If not ``null``, a button in the app should link to this page.
|
||||
├ privacy_url string URL to privacy notice page. If not ``null``, a button in the app should link to this page.
|
||||
├ help_url string URL to help page. If not ``null``, a button in the app should link to this page.
|
||||
@@ -596,6 +615,7 @@ scan_types list of objects Only used for a
|
||||
"booth": "A2",
|
||||
"event": {
|
||||
"name": {"en": "Sample conference", "de": "Beispielkonferenz"},
|
||||
"end_date": "2017-12-28T10:00:00+00:00",
|
||||
"slug": "bigevents",
|
||||
"imprint_url": null,
|
||||
"privacy_url": null,
|
||||
@@ -634,6 +654,7 @@ On the request, you should set the following properties:
|
||||
* ``tags`` with the list of selected tags
|
||||
* ``rating`` with the rating assigned by the exhibitor
|
||||
* ``device_name`` with a user-specified name of the device used for scanning (max. 190 characters), or ``null``
|
||||
* ``device_uuid`` with a auto-generated UUID of the device used for scanning, or ``null``
|
||||
|
||||
If you submit ``tags`` and ``rating`` to be ``null`` and ``notes`` to be ``""``, the server
|
||||
responds with the previously saved information and will not delete that information. If you
|
||||
@@ -668,7 +689,8 @@ The request for this looks like this:
|
||||
"scan_type": "lead",
|
||||
"tags": ["foo"],
|
||||
"rating": 4,
|
||||
"device_name": "DEV1"
|
||||
"device_name": "DEV1",
|
||||
"device_uuid": "d8c2ec53-d602-4a08-882d-db4cf54344a2"
|
||||
}
|
||||
|
||||
**Example response:**
|
||||
@@ -701,7 +723,9 @@ The request for this looks like this:
|
||||
},
|
||||
"rating": 4,
|
||||
"tags": ["foo"],
|
||||
"notes": "Great customer, wants our newsletter"
|
||||
"notes": "Great customer, wants our newsletter",
|
||||
"device_name": "DEV1",
|
||||
"device_uuid": "d8c2ec53-d602-4a08-882d-db4cf54344a2"
|
||||
}
|
||||
|
||||
:statuscode 200: No error, leads was not scanned for the first time
|
||||
@@ -756,7 +780,9 @@ You can also fetch existing leads (if you are authorized to do so):
|
||||
},
|
||||
"rating": 4,
|
||||
"tags": ["foo"],
|
||||
"notes": "Great customer, wants our newsletter"
|
||||
"notes": "Great customer, wants our newsletter",
|
||||
"device_name": "DEV1",
|
||||
"device_uuid": "d8c2ec53-d602-4a08-882d-db4cf54344a2"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -24,3 +24,4 @@ If you want to **create** a plugin, please go to the
|
||||
imported_secrets
|
||||
webinar
|
||||
presale-saml
|
||||
kulturpass
|
||||
|
||||
193
doc/plugins/kulturpass.rst
Normal file
193
doc/plugins/kulturpass.rst
Normal file
@@ -0,0 +1,193 @@
|
||||
KulturPass
|
||||
=========
|
||||
|
||||
.. note::
|
||||
|
||||
Since the KulturPass is specific to event organizers within Germany, the following page is also only provided in
|
||||
German. Should you require assistance with the KulturPass and do not speak this language, please feel free reach
|
||||
out to support@pretix.eu.
|
||||
|
||||
|
||||
Einführung
|
||||
----------
|
||||
Der `KulturPass`_ ist ein Angebot der Bundesregierung für alle, die im laufenden Jahr ihren 18. Geburtstag feiern.
|
||||
Sie erhalten ab ihrem 18. Geburtstag ein Budget von 200 Euro, das sie für Eintrittskarten, Bücher, CDs, Platten und
|
||||
vieles andere einsetzen können. So wird Kultur vor Ort noch einfacher erlebbar. Gleichzeitig stärkt das die Nachfrage
|
||||
bei den Anbietenden.
|
||||
|
||||
Da pretix ein Ticketing-System ist, stellen wir ausschließlich einen automatisierten Prozess für den Verkauf von
|
||||
Eintrittskarten über den KulturPass-Marktplatz bereit.
|
||||
|
||||
|
||||
Registrierung und Einrichtung
|
||||
-----------------------------
|
||||
Um als Unternehmen oder Kultureinrichtung Angebote auf dem KulturPass-Marktplatz anbieten zu können, ist zunächst eine
|
||||
Registerung und die Einrichtung eines "Shops" sowie der dazugehörigen Angebote notwendig.
|
||||
|
||||
1. Registrierung
|
||||
Registrieren Sie sich zunächst unter https://www.kulturpass.de/anbietende/layer als Anbieter. Im Zuge der
|
||||
Registrierung beantworten Sie einige Fragen zu Ihrem Unternehmen/Ihrer Kultureinrichtung, hinterlegen Ihre
|
||||
E-Mail-Adresse und beantworten Fragen zu Ihren Angebotsformen sowie Finanzierung Ihrer Einrichtung.
|
||||
|
||||
2. Anlegen eines KulturPass Shops
|
||||
Nach Ihrer Registrierung müssen Sie der Weitergabe Ihrer Daten an die technische Platform hinter dem KulturPass,
|
||||
Mirakl, zustimmen. Hier benennen Sie auch Ihren Shop.
|
||||
|
||||
3. Identifizierung mit ELSTER-Zertifikat
|
||||
Als nächsten Schritt müssen Sie Ihr Unternehmen oder Ihre Einrichtung mit Hilfe eines sog. ELSTER-Zertifikates
|
||||
identifizieren. Dieses Zertifikat nutzen Sie auch bereits jetzt schon, wenn Sie auf elektronischem Wege mit der
|
||||
Finanzverwaltung kommunizieren.
|
||||
|
||||
4. Ersteinrichtung in pretix
|
||||
Hinterlegen Sie nun die ID-Nummer Ihres KulturPass Marktplatz-Shops sowie einen API-Key in den
|
||||
`Einstellungen Ihres Veranstalterkontos`_ (Veranstalter-Konto -> Einstellungen -> KulturPass). Diese Daten müssen
|
||||
Sie nur einmalig für alle Ihre Veranstaltungen angeben.
|
||||
|
||||
Im `KulturPass-Backend`_ finden Sie die benötigten Informationen indem Sie auf das Benutzer-Symbol in der oberen,
|
||||
rechten Ecke klicken, "Profil" und dann "API Schlüssel" auswählen bzw. indem Sie auf "Einstellungen" in der
|
||||
Navigation links und dann "Shop" auswählen.
|
||||
|
||||
.. note::
|
||||
|
||||
Zu jedem Zeitpunkt kann nur ein Hintergrundsystem mit dem KulturPass-System verbunden sein. Werden
|
||||
unterschiedliche Systeme oder gar mehrere pretix-Veranstalterkonten mit dem gleichen KulturPass-System verbunden,
|
||||
können keine Bestellungen mehr verarbeitet werden und Angebote nicht automatisiert an den KulturPass-Marktplatz
|
||||
übermittelt werden. Eingehende Bestellungen von Jugendlichen werden in diesem Fall automatisch abgelehnt, da diese
|
||||
nicht eindeutig zugeordnet werden können. Ebenso überschreibt die Bereitstellung der Angebote eines Systems die
|
||||
Angebote eines anderen Systems.
|
||||
|
||||
Wenn Sie mehrere Systeme haben, die den KulturPass-Marktplatz bedienen sollen, wenden Sie sich bitte an den
|
||||
KulturPass-Support, um sich einen weiteren Shop einrichten zu lassen.
|
||||
|
||||
5. Aktivierung der KulturPass-Erweiterungen
|
||||
Alle Veranstaltungen, die Sie über den KulturPass anbieten möchten, benötigen die `KulturPass-Erweiterung`_.
|
||||
Aktivieren Sie diese bitte in jeder relevanten Veranstaltung über Einstellungen -> Erweiterungen -> Tab
|
||||
"Integrationen" -> KulturPass.
|
||||
|
||||
6. Konfiguration der Artikel
|
||||
Nachdem die KulturPass-Erweiterung aktiviert wurde, müssen Sie sich entscheiden, welche Produkte Sie über den
|
||||
KulturPass-Marktplatz anbieten möchten. In der Bearbeitungs-Ansicht des jeweiligen Produktes finden Sie hierzu im
|
||||
Tab "Zusätzliche Einstellungen" eine Checkbox "Das Produkt kann mit dem KulturPass erworben werden".
|
||||
|
||||
.. note::
|
||||
|
||||
Die Eigenschaft, dass ein Produkt durch den KulturPass-Marktplatz erworben werden kann, kann für beliebig viele
|
||||
Produkte aktiviert werden. Auf Grund der Funktionsweise des KulturPasses sollten Sie jedoch gerade bei vielen
|
||||
Artikeln mit unterschiedlich hohen Preisen darauf achten, dass die Preisspanne nicht zu hoch ausfällt.
|
||||
|
||||
Aktivieren Sie die Option für drei Produkte für 1, 10 und 100 Euro, so wird Ihr Angebot im KulturPass-Marktplatz
|
||||
für 100 Euro gelistet werden. Dies bedeutet im Umkehrschluss auch, dass das KulturPass-Guthaben eines Jugendlichen
|
||||
auch mindestens 100 Euro betragen muss, damit er Ihr Angebot in Anspruch nehmen kann - auch wenn die betroffene
|
||||
Person lediglich das 1 Euro-Angebot wahrnehmen möchte. Erst mit dem 100 Euro KulturPass-Einlösecode wählt die
|
||||
kaufende Person in Ihrem pretix-Shop aus, welches Produkt erworben werden soll. Ein Restguthaben wird nach dem Kauf
|
||||
automatisch zurückerstattet und dem KulturPass-Konto wieder gutgeschrieben.
|
||||
|
||||
7. Konfiguration des Marktplatz-Eintrages
|
||||
Je nach dem, ob es sich bei Ihrer Veranstaltung um eine Einzelveranstaltung oder eine Veranstaltungsreihe handelt,
|
||||
müssen Sie die folgende Einstellung einmalig oder pro Veranstaltungstermin vornehmen.
|
||||
|
||||
Einzelveranstaltungen konfigurieren Sie über den Menüpunkt "KulturPass" in den Einstellungen Ihrer Veranstaltung;
|
||||
Veranstaltungsreihen beim Anlegen oder Editieren eines jeden einzelnen Termins am Ende der Seite.
|
||||
|
||||
Um eine Veranstaltung oder einen Veranstaltungstermin im KulturPass-Marktplatz anzubieten, aktivieren Sie zunächst
|
||||
die Option "Diese Veranstaltung via KulturPass anbieten". Geben Sie im folgenden die benötigten Informationen an.
|
||||
|
||||
Bitte beachten Sie, dass Sie bei den Angaben präzise Titel und Beschreibungen verwenden, da der KulturPass-
|
||||
Marktplatz ausschließlich die Informationen aus diesem Bereich verwendet. Etwaige andere Informationen die Sie
|
||||
bspw. in den "Text auf Startseite"-Felder eingeben haben, erreichen das KulturPass-System nicht.
|
||||
|
||||
.. note::
|
||||
|
||||
Gerade bei Veranstaltungsreihen nutzen viele pretix-Veranstalter gerne verkürzte Termin-Namen. Ein Schwimmbad würde
|
||||
beispielsweise Ihre Veranstaltungsreihe "Freibad Musterstadt" und die einzelnen Termine nur "Schwimmen" nennen.
|
||||
|
||||
Während dies im pretix-Shop in einem gemeinsamen Kontext wunderbar funktioniert, würde eine Veranstaltung mit dem
|
||||
Titel "Schwimmen" im KulturPass-Marktplatz Informationen vermissen lassen. Wählen Sie daher für das Eingabefeld
|
||||
"Veranstaltungstitel" in der KulturPass-Konfiguration einen sprechenden Wert.
|
||||
|
||||
8. Übermittlung der Angebote
|
||||
Sobald Sie Ihre ersten Veranstaltungen konfiguriert und live geschaltet haben, übermittelt pretix automatisch in
|
||||
regelmäßigen Abständen alle von Ihnen angebotenen Veranstaltungen an das KulturPass System (Mirakl). Bitte beachten
|
||||
Sie jedoch, dass der Import der Produkte und Angebote einige Zeit in Anspruch nehmen kann. Zum einen müssen
|
||||
Angebote initial händisch von den Betreibern der KulturPass-Platform freigegeben werden, zum anderen muss auch eine
|
||||
Synchronisation zwischen dem Hintergrundsystem und der KulturPass-App erfolgen. Auf die Dauer dieser Prozesse hat
|
||||
pretix keinen Einfluss.
|
||||
|
||||
9. Freischalten des Marktplatz-Shops
|
||||
Nachdem pretix erstmalig Angebote an das KulturPass-System übermittelt hat, müssen Sie Ihren Shop KulturPass-Shop
|
||||
einmalig freischalten. Loggen Sie sich hierzu in das `KulturPass-Backend`_ ein.
|
||||
|
||||
|
||||
Verwalten von KulturPass-Bestellungen
|
||||
-------------------------------------
|
||||
Durch die Nutzung der pretix-Integration mit dem KulturPass-System müssen Sie sich - bis auf die Kennzeichnung von
|
||||
Produkten, die per KulturPass erworben werden dürfen, sowie die Bereitstellung von Veranstaltungs-Informationen für den
|
||||
KulturPass-Marktplatz - um nichts kümmern: pretix übermittelt automatisch Ihre Veranstaltungen, wickelt die Einlösung
|
||||
der Tickets ab und führt die Abrechnung mit dem Hintergrund-System durch.
|
||||
|
||||
Für Ihre Kunden verhält sich der KulturPass wie eine Zahlungsmethode im Bestellprozess und wird dort neben Ihren
|
||||
anderen Zahlungsmethoden mit angeboten.
|
||||
|
||||
Die Gelder für mit dem KulturPass bezahlte Tickets erhalten Sie in Form einer Sammel-Überweisung von der Stiftung
|
||||
Digitale Chancen auf das von Ihnen beim KulturPass Onboarding angegeben Bankkonto.
|
||||
|
||||
In Ihrem `KulturPass-Backend`_ können Sie über den Menüpunkt "Buchhaltung" Ihre bereits erfolgten und kommenden
|
||||
Auszahlungen betrachten.
|
||||
|
||||
.. note::
|
||||
|
||||
Es ist von äußerster Wichtigkeit, dass Sie weder die eingehenden Bestellungen noch die Produkte und Angebote im
|
||||
KulturPass-Backend händisch bearbeiten - auch wenn dies möglich wäre.
|
||||
|
||||
Bei händischen Änderungen riskieren Sie, dass die Datenbasis zwischen pretix und dem KulturPass-System divergiert
|
||||
und es zu fehlerhaften Buchungen kommt. Wann immer möglich, sollten Sie Korrekturbuchungen und Änderungen
|
||||
ausschließlich über pretix vornehmen.
|
||||
|
||||
Sollte eine händische Änderung/Korrektur notwendig werden, wenden Sie sich bitte an den pretix-Support, damit wir
|
||||
die Auswirkungen evaluieren und vorab mit Ihnen besprechen können!
|
||||
|
||||
Erstattungen für Stornos und Absagen können Sie wie gehabt über das pretix-Backend vornehmen. Der jeweilige Betrag wird
|
||||
dem KulturPass-Konto dann automatisch gutgeschrieben.
|
||||
|
||||
Da nach Ausgabe eines KulturPass Einlöse-Codes dieser vom Kunden jederzeit oder vom System bei
|
||||
Nicht-(Komplett)Einlösung binnen 48 Stunden storniert werden kann, kann das im KulturPass-Backend angezeigte,
|
||||
auszuzahlende Guthaben fluktuieren. Da in der Regel Auszahlungen frühestens 48 Stunden nach der Aufgabe einer
|
||||
KulturPass-Bestellungen erfolgen, sollte Ihr Guthaben in der Regel nicht ins Negative gehen.
|
||||
|
||||
Ablauf für Kunden
|
||||
-----------------
|
||||
Ihre Kunden erhalten - nachdem sie sich ein eigenes Konto in der KulturPass-App angelegt und sich mit ihrem
|
||||
elektronischen Personalausweis identifiziert haben - ein Guthaben von 200 Euro, welches für Leistungen aus dem
|
||||
KulturPass-Marktplatz eingelöst werden kann.
|
||||
|
||||
Im Falle von Veranstaltungen, die per pretix verkauft werden, wählt der Kunde ein Angebot aus und erhält im folgenden
|
||||
binnen kurzer Zeit (ca. 10-20 Minuten) einen Code und einen Link, um diesen einzulösen. Der Link bringt den Kunden direkt auf die Seite der
|
||||
betreffenden pretix-Veranstaltung. Hier wird der Kunde darauf hingewiesen, für welche Produkte der Code genutzt werden
|
||||
kann.
|
||||
|
||||
Im Bezahlschritt des Verkaufsprozesses wird dem Kunden vorgeschlagen, seinen KulturPass Einlösecode nun zu nutzen, um
|
||||
die gewünschte Leistung zu erhalten.
|
||||
|
||||
Wurde ein Artikel gewählt, welcher günstiger als der Wert des Einlösecodes war, wird das Restguthaben automatisch auf
|
||||
das KulturPass-Konto erstattet.
|
||||
|
||||
Wurden hingegen mehrere Artikel in den Warenkorb gelegt, so kann die Differenz mit einem anderen, regulären
|
||||
Zahlungsmittel erfolgen.
|
||||
|
||||
Einlösecodes, die vom Kunden nicht binnen 48 Stunden eingelöst werden, werden automatisch storniert und dem
|
||||
KulturPass-Konto wieder gutgeschrieben. Dieser Mechanismus greift auch, wenn eine Veranstaltung mittlerweile
|
||||
ausverkauft ist und daher der Einlösecode nicht mehr Nutzbar ist.
|
||||
|
||||
|
||||
Unterstützung
|
||||
-------------
|
||||
Weitergehende Informationen zum KulturPass finden Sie auch auf der `Webseite des KulturPasses`_, sowie im
|
||||
`KulturPass Serviceportal`_.
|
||||
|
||||
|
||||
.. _KulturPass: https://www.kulturpass.de/
|
||||
.. _Einstellungen Ihres Veranstalterkontos: https://pretix.eu/control/organizer/-/settings/kulturpass
|
||||
.. _KulturPass-Erweiterung: https://pretix.eu/control/event/-/-/settings/plugins#tab-0-2-open
|
||||
.. _KulturPass-Backend: https://kulturpass-de.mirakl.net/
|
||||
.. _Webseite des KulturPasses: https://www.kulturpass.de/
|
||||
.. _KulturPass Serviceportal: https://service.kulturpass.de/help/
|
||||
@@ -1,4 +1,4 @@
|
||||
sphinx==7.0.*
|
||||
sphinx==7.3.*
|
||||
jinja2==3.1.*
|
||||
sphinx-rtd-theme
|
||||
sphinxcontrib-httpdomain
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
-e ../
|
||||
sphinx==7.0.*
|
||||
sphinx==7.3.*
|
||||
jinja2==3.1.*
|
||||
sphinx-rtd-theme
|
||||
sphinxcontrib-httpdomain
|
||||
|
||||
113
doc/user/android-version-support.rst
Normal file
113
doc/user/android-version-support.rst
Normal file
@@ -0,0 +1,113 @@
|
||||
Android version support policy
|
||||
==============================
|
||||
|
||||
Building software for Android always presents a struggle between keeping compatibility with older hardware to save cost
|
||||
and utilizing feature of new Android versions to improve functionality, security and stability. To help you plan ahead,
|
||||
we are publishing our intended schedule. This is to be understood as a minimum commitment, we will only drop support for
|
||||
older versions if there is a technical reason to do so, not because the scheduled time has been reached.
|
||||
|
||||
.. warning:: This is a non-binding document. We will try our very best to not to deprecate support for Android versions
|
||||
earlier than listed here, but for technical or economical reasons, it might become necessary to do so under
|
||||
specific circumstances. Specifically, we might be forced to partially drop support for Android versions
|
||||
earlier where we integrate third-party components into our software. Typical examples would be specific
|
||||
payment terminal or printer types where we use a third-party component provided by the hardware vendor.
|
||||
|
||||
If we no longer support an Android version, it means that we will no longer publish new versions of the app supporting
|
||||
that Android version. This means you are not getting new features or bug fixes, and at some point your app might stop
|
||||
working with the pretix server.
|
||||
|
||||
pretixSCAN
|
||||
----------
|
||||
|
||||
=========================== ==========================================================
|
||||
Android Version Support schedule
|
||||
=========================== ==========================================================
|
||||
Android 14 Support planned until at least 12/2029.
|
||||
Android 13 Support planned until at least 12/2028.
|
||||
Android 12 Support planned until at least 12/2027.
|
||||
Android 11 Support planned until at least 12/2026.
|
||||
Android 10 Support planned until at least 12/2025.
|
||||
Android 9 Support planned until at least 12/2025.
|
||||
Android 8 Support planned until at least 12/2025.
|
||||
Android 7 Support planned until at least 06/2025.
|
||||
Android 6 Support planned until at least 06/2025.
|
||||
Android 5 | Support planned until at least 06/2025.
|
||||
| No support for COVID certificate verification.
|
||||
Android 4 Support dropped.
|
||||
=========================== ==========================================================
|
||||
|
||||
pretixPOS
|
||||
---------
|
||||
|
||||
=========================== ==========================================================
|
||||
Android Version Support schedule
|
||||
=========================== ==========================================================
|
||||
Android 14 | Support planned until at least 12/2029.
|
||||
| Limited support for Swissbit microSD TSE (only tested devices).
|
||||
Android 13 | Support planned until at least 12/2028.
|
||||
| Limited support for Swissbit microSD TSE (only tested devices).
|
||||
Android 12 | Support planned until at least 12/2027.
|
||||
| Limited support for Swissbit microSD TSE (only tested devices).
|
||||
Android 11 | Support planned until at least 12/2026.
|
||||
| No support for Swissbit microSD TSE.
|
||||
Android 10 Support planned until at least 12/2025.
|
||||
Android 9 Support planned until at least 12/2025.
|
||||
Android 8 | Support planned until at least 12/2025.
|
||||
| Support for Stripe Terminal on some devices to be dropped 05/2024.
|
||||
Android 7 | Support planned until at least 12/2024.
|
||||
| Support for Stripe Terminal to be dropped 05/2024.
|
||||
| No support for Cryptovision TSE.
|
||||
Android 6 | Support planned until at least 12/2024.
|
||||
| No support for Cryptovision TSE.
|
||||
| No support for Fiskal Cloud.
|
||||
| No support for Stripe Terminal.
|
||||
Android 5 | Support planned until at least 12/2024.
|
||||
| No support for Cryptovision TSE.
|
||||
| No support for Fiskal Cloud.
|
||||
| No support for Stripe Terminal.
|
||||
| No support for SumUp.
|
||||
| No support for COVID certificate verification.
|
||||
Android 4 Support dropped.
|
||||
=========================== ==========================================================
|
||||
|
||||
pretixPRINT
|
||||
-----------
|
||||
|
||||
=========================== ==========================================================
|
||||
Android Version Support schedule
|
||||
=========================== ==========================================================
|
||||
Android 14 Support planned until at least 12/2029.
|
||||
Android 13 Support planned until at least 12/2028.
|
||||
Android 12 Support planned until at least 12/2027.
|
||||
Android 11 Support planned until at least 12/2026.
|
||||
Android 10 Support planned until at least 12/2025.
|
||||
Android 9 Support planned until at least 12/2025.
|
||||
Android 8 Support planned until at least 12/2025.
|
||||
Android 7 Support planned until at least 06/2025.
|
||||
Android 6 Support planned until at least 06/2025.
|
||||
Android 5 | Support planned until at least 06/2025.
|
||||
| No support for Evolis printers on some devices.
|
||||
Android 4.4 | Support planned until at least 06/2024.
|
||||
| No support for USB printers.
|
||||
| No support for Evolis printers.
|
||||
Android 4 Support dropped.
|
||||
=========================== ==========================================================
|
||||
|
||||
pretixLEAD
|
||||
----------
|
||||
|
||||
=========================== ==========================================================
|
||||
Android Version Support schedule
|
||||
=========================== ==========================================================
|
||||
Android 14 Support planned until at least 12/2029.
|
||||
Android 13 Support planned until at least 12/2028.
|
||||
Android 12 Support planned until at least 12/2027.
|
||||
Android 11 Support planned until at least 12/2026.
|
||||
Android 10 Support planned until at least 12/2025.
|
||||
Android 9 Support planned until at least 12/2025.
|
||||
Android 8 Support planned until at least 12/2025.
|
||||
Android 7 Support planned until at least 12/2024.
|
||||
Android 6 Support planned until at least 12/2024.
|
||||
Android 5 Support planned until at least 12/2024.
|
||||
Android 4 Support dropped.
|
||||
=========================== ==========================================================
|
||||
@@ -194,17 +194,23 @@ A complete record could look like this::
|
||||
|
||||
v=spf1 a mx include:_spf.pretix.eu ~all
|
||||
|
||||
Make sure to read up on the `SPF specification`_. If you want to authenticate your emails with DKIM, set up a DNS TXT
|
||||
record for the subdomain ``pretix._domainkey`` with the following contents::
|
||||
Make sure to read up on the `SPF specification`_.
|
||||
|
||||
v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDXrDk6lwOWX00e2MbiiJac6huI+gnzLf9N4G1FnBv3PXq8fz3i2q1szH72OF5mAlKm3zXO4cl/uxx+lfidS1ERbX6Bn9BRstBTQUKWC4JFj8Yk9+fwT7LWehDURazLdTzfsIjJFudLLvxtOKSaOCtMhbPX05DIhziaqVCBqgz/NQIDAQAB
|
||||
If you want to authenticate your emails with `DKIM`_, set up a ``CNAME`` record for the subdomain ``pretix._domainkey``
|
||||
pointing to ``dkim.pretix.eu``::
|
||||
|
||||
pretix._domainkey.mydomain.com. CNAME dkim.pretix.eu.
|
||||
|
||||
Then, please contact support@pretix.eu and we will enable DKIM for your domain on our mail servers.
|
||||
|
||||
For senders with larger volumes, Google Mail also requires you to have a `DMARC`_ policy (that may however be ``p=none``).
|
||||
|
||||
.. note:: Many SMTP servers impose rate limits on the sent emails, such as a maximum number of emails sent per hour.
|
||||
These SMTP servers are often not suitable for use with pretix, in case you want to send an email to many
|
||||
hundreds or thousands of ticket buyers. Depending on how the rate limit is implemented, emails might be lost
|
||||
in this case, as pretix only retries email delivery for a certain time period.
|
||||
|
||||
.. _DKIM: https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail
|
||||
.. _Sender Policy Framework: https://en.wikipedia.org/wiki/Sender_Policy_Framework
|
||||
.. _SPF specification: http://www.open-spf.org/SPF_Record_Syntax
|
||||
.. _DMARC: https://en.wikipedia.org/wiki/DMARC
|
||||
@@ -19,4 +19,3 @@ Then, head to the **Bundled products** tab of the "conference ticket" and add th
|
||||
|
||||
Once a customer tries to buy the € 450 conference ticket, a sub-product will be added and the price will automatically be split into the two components, leading to a correct computation of taxes.
|
||||
|
||||
You can find more use cases in these specialized guides:
|
||||
|
||||
@@ -17,8 +17,8 @@ and then click "Generate widget code".
|
||||
You will obtain two code snippets that look *roughly* like the following. The first should be embedded into the
|
||||
``<head>`` part of your website, if possible. If this inconvenient, you can put it in the ``<body>`` part as well::
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="https://pretix.eu/demo/democon/widget/v1.css">
|
||||
<script type="text/javascript" src="https://pretix.eu/widget/v1.en.js" async></script>
|
||||
<link rel="stylesheet" type="text/css" href="https://pretix.eu/demo/democon/widget/v1.css" crossorigin>
|
||||
<script type="text/javascript" src="https://pretix.eu/widget/v1.en.js" async crossorigin></script>
|
||||
|
||||
The second snippet should be embedded at the position where the widget should show up::
|
||||
|
||||
@@ -138,7 +138,7 @@ the button-style of that checkbox with the one in the pretix shop, you can use t
|
||||
|
||||
.. note::
|
||||
|
||||
Due to compatibilty with existing widget installations, the default value for ``single-item-select``
|
||||
Due to compatibility with existing widget installations, the default value for ``single-item-select``
|
||||
is ``checkbox``. This might change in the future, so make sure, to set the attribute to
|
||||
``single-item-select="checkbox"`` if you need it.
|
||||
|
||||
@@ -196,6 +196,10 @@ settings. For example, if you set up a meta data property called "Promoted" that
|
||||
|
||||
<pretix-widget event="https://pretix.eu/demo/series/" list-type="list" filter="attr[Promoted]=Yes"></pretix-widget>
|
||||
|
||||
If you have enabled public filters in your meta data attribute configuration, a filter-form shows up. To disable, use::
|
||||
|
||||
<pretix-widget event="https://pretix.eu/demo/democon/" disable-filters></pretix-widget>
|
||||
|
||||
pretix Button
|
||||
-------------
|
||||
|
||||
@@ -425,4 +429,34 @@ Hosted or pretix Enterprise are active, you can pass the following fields:
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
Offering wallet payments (Apple Pay, Google Pay) within the widget
|
||||
------------------------------------------------------------------
|
||||
|
||||
Some payment providers (such as Stripe) also offer Apple or Google Pay. But in order to use them, the domain of the
|
||||
payment needs to be approved first. As of right now, pretix will take care of the domain verification process for you
|
||||
automatically, when using Stripe. However, pretix can only validate the domain that is being used for your default,
|
||||
"stand-alone" shop (such as https://pretix.eu/demo/democon/ ).
|
||||
|
||||
When embedding the widget on your website, the domain of the embedding page will also need to be validated in order to
|
||||
be able to use it for wallet payments.
|
||||
|
||||
The details might vary from payment provider to payment provider, but generally speaking, it will either involve just
|
||||
telling your payment provider the domain name and (for Apple Pay) placing an
|
||||
``apple-developer-merchantid-domain-association``-file into the ``.well-known``-directory of your domain.
|
||||
|
||||
Further reading:
|
||||
|
||||
* `Stripe Payment Method Domain registration`_
|
||||
|
||||
Working with Cross-Origin-Embedder-Policy
|
||||
-----------------------------------------
|
||||
|
||||
The pretix widget is unfortunately not compatible with ``Cross-Origin-Embedder-Policy: require-corp``. If you include
|
||||
the ``crossorigin`` attributes on the ``<script>`` and ``<link>`` tag as shown above, the widget can show a calendar
|
||||
or product list, but will not be able to open the checkout process in an iframe. If you also set
|
||||
``Cross-Origin-Opener-Policy: same-origin``, the widget can auto-detect that it is running in an isolated enviroment
|
||||
and will instead open the checkout process in a new tab.
|
||||
|
||||
.. _Let's Encrypt: https://letsencrypt.org/
|
||||
.. _Stripe Payment Method Domain registration: https://stripe.com/docs/payments/payment-methods/pmd-registration
|
||||
|
||||
@@ -16,4 +16,5 @@ wanting to use pretix to sell tickets.
|
||||
events/giftcards
|
||||
faq
|
||||
markdown
|
||||
android-version-support
|
||||
glossary
|
||||
|
||||
@@ -11,6 +11,9 @@ In many places of your shop, like frontpage texts, product descriptions and emai
|
||||
since it is way easier to learn than languages like HTML but allows all basic formatting options required
|
||||
for text in those places.
|
||||
|
||||
.. note:: Some fields that are used in one-line context only allow formatting that refers to individual words
|
||||
(such as bold or italic font or a link) but do not allow block-level formatting like lists or headlines.
|
||||
|
||||
Formatting rules
|
||||
----------------
|
||||
|
||||
@@ -145,7 +148,7 @@ to get a better plain text representation of your text. Note however, that for
|
||||
security reasons you can only use the following HTML elements::
|
||||
|
||||
a, abbr, acronym, b, br, code, div, em, h1, h2,
|
||||
h3, h4, h5, h6, hr, i, li, ol, p, pre, span, strong,
|
||||
h3, h4, h5, h6, hr, i, li, ol, p, pre, s, span, strong,
|
||||
table, tbody, td, thead, tr, ul
|
||||
|
||||
Additionally, only the following attributes are allowed on them::
|
||||
|
||||
@@ -30,42 +30,42 @@ dependencies = [
|
||||
"babel",
|
||||
"BeautifulSoup4==4.12.*",
|
||||
"bleach==5.0.*",
|
||||
"celery==5.3.*",
|
||||
"chardet==5.1.*",
|
||||
"celery==5.4.*",
|
||||
"chardet==5.2.*",
|
||||
"cryptography>=3.4.2",
|
||||
"css-inline==0.8.*",
|
||||
"css-inline==0.14.*",
|
||||
"defusedcsv>=1.1.0",
|
||||
"dj-static",
|
||||
"Django==4.2.*",
|
||||
"django-bootstrap3==23.1.*",
|
||||
"django-compressor==4.3.*",
|
||||
"django-countries==7.5.*",
|
||||
"django-filter==23.2",
|
||||
"django-bootstrap3==24.2",
|
||||
"django-compressor==4.4",
|
||||
"django-countries==7.6.*",
|
||||
"django-filter==24.2",
|
||||
"django-formset-js-improved==0.5.0.3",
|
||||
"django-formtools==2.4.1",
|
||||
"django-formtools==2.5.1",
|
||||
"django-hierarkey==1.1.*",
|
||||
"django-hijack==3.3.*",
|
||||
"django-hijack==3.4.*",
|
||||
"django-i18nfield==1.9.*,>=1.9.4",
|
||||
"django-libsass==0.9",
|
||||
"django-localflavor==4.0",
|
||||
"django-markup",
|
||||
"django-oauth-toolkit==2.2.*",
|
||||
"django-otp==1.2.*",
|
||||
"django-phonenumber-field==7.1.*",
|
||||
"django-redis==5.2.*",
|
||||
"django-oauth-toolkit==2.3.*",
|
||||
"django-otp==1.5.*",
|
||||
"django-phonenumber-field==7.3.*",
|
||||
"django-redis==5.4.*",
|
||||
"django-scopes==2.0.*",
|
||||
"django-statici18n==2.3.*",
|
||||
"djangorestframework==3.14.*",
|
||||
"dnspython==2.3.*",
|
||||
"django-statici18n==2.5.*",
|
||||
"djangorestframework==3.15.*",
|
||||
"dnspython==2.6.*",
|
||||
"drf_ujson2==1.7.*",
|
||||
"geoip2==4.*",
|
||||
"importlib_metadata==6.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
||||
"importlib_metadata==7.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
||||
"isoweek",
|
||||
"jsonschema",
|
||||
"kombu==5.3.*",
|
||||
"libsass==0.22.*",
|
||||
"libsass==0.23.*",
|
||||
"lxml",
|
||||
"markdown==3.4.3", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
|
||||
"markdown==3.6", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
|
||||
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
|
||||
"mt-940==4.30.*",
|
||||
"oauthlib==3.2.*",
|
||||
@@ -73,63 +73,61 @@ dependencies = [
|
||||
"packaging",
|
||||
"paypalrestsdk==1.13.*",
|
||||
"paypal-checkout-serversdk==1.0.*",
|
||||
"PyJWT==2.7.*",
|
||||
"PyJWT==2.8.*",
|
||||
"phonenumberslite==8.13.*",
|
||||
"Pillow==9.5.*",
|
||||
"Pillow==10.3.*",
|
||||
"pretix-plugin-build",
|
||||
"protobuf==4.23.*",
|
||||
"protobuf==5.26.*",
|
||||
"psycopg2-binary",
|
||||
"pycountry",
|
||||
"pycparser==2.21",
|
||||
"pycryptodome==3.18.*",
|
||||
"pycparser==2.22",
|
||||
"pycryptodome==3.20.*",
|
||||
"pypdf==3.9.*",
|
||||
"python-bidi==0.4.*", # Support for Arabic in reportlab
|
||||
"python-dateutil==2.8.*",
|
||||
"python-u2flib-server==4.*",
|
||||
"python-dateutil==2.9.*",
|
||||
"pytz",
|
||||
"pytz-deprecation-shim==0.1.*",
|
||||
"pyuca",
|
||||
"qrcode==7.4.*",
|
||||
"redis==4.6.*",
|
||||
"reportlab==4.0.*",
|
||||
"redis==5.0.*",
|
||||
"reportlab==4.2.*",
|
||||
"requests==2.31.*",
|
||||
"sentry-sdk==1.15.*",
|
||||
"sentry-sdk==1.45.*",
|
||||
"sepaxml==2.6.*",
|
||||
"slimit",
|
||||
"static3==0.7.*",
|
||||
"stripe==5.4.*",
|
||||
"stripe==7.9.*",
|
||||
"text-unidecode==1.*",
|
||||
"tlds>=2020041600",
|
||||
"tqdm==4.*",
|
||||
"ua-parser==0.18.*",
|
||||
"vat_moss_forked==2020.3.20.0.11.0",
|
||||
"vobject==0.9.*",
|
||||
"webauthn==0.4.*",
|
||||
"webauthn==2.1.*",
|
||||
"zeep==4.2.*"
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
memcached = ["pylibmc"]
|
||||
dev = [
|
||||
"aiohttp==3.8.*",
|
||||
"aiohttp==3.9.*",
|
||||
"coverage",
|
||||
"coveralls",
|
||||
"fakeredis==2.18.*",
|
||||
"flake8==6.0.*",
|
||||
"fakeredis==2.22.*",
|
||||
"flake8==7.0.*",
|
||||
"freezegun",
|
||||
"isort==5.12.*",
|
||||
"isort==5.13.*",
|
||||
"pep8-naming==0.13.*",
|
||||
"potypo",
|
||||
"pycodestyle==2.10.*",
|
||||
"pyflakes==3.0.*",
|
||||
"pytest-asyncio",
|
||||
"pytest-cache",
|
||||
"pytest-cov",
|
||||
"pytest-django==4.*",
|
||||
"pytest-mock==3.10.*",
|
||||
"pytest-rerunfailures==11.*",
|
||||
"pytest-mock==3.14.*",
|
||||
"pytest-rerunfailures==14.*",
|
||||
"pytest-sugar",
|
||||
"pytest-xdist==3.3.*",
|
||||
"pytest==7.3.*",
|
||||
"pytest-xdist==3.6.*",
|
||||
"pytest==8.2.*",
|
||||
"responses",
|
||||
]
|
||||
|
||||
|
||||
4
src/.watchmanconfig
Normal file
4
src/.watchmanconfig
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"ignore_dirs": ["node_modules", "data", "pretix/static", "pretix/locale", "pretix/static.dist"]
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ localecompile:
|
||||
./manage.py compilemessages
|
||||
|
||||
localegen:
|
||||
./manage.py makemessages --keep-pot --ignore "pretix/helpers/*" --ignore "pretix/static/npm_dir/*" $(LNGS)
|
||||
./manage.py makemessages --keep-pot --ignore "pretix/static/npm_dir/*" $(LNGS)
|
||||
./manage.py makemessages --keep-pot -d djangojs --ignore "pretix/static/npm_dir/*" --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "pretix/static/rrule/*" --ignore "build/*" $(LNGS)
|
||||
|
||||
staticfiles: jsi18n
|
||||
|
||||
@@ -19,4 +19,4 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
__version__ = "2023.9.0.dev0"
|
||||
__version__ = "2024.5.0.dev0"
|
||||
|
||||
@@ -92,6 +92,7 @@ ALL_LANGUAGES = [
|
||||
('id', _('Indonesian')),
|
||||
('it', _('Italian')),
|
||||
('lv', _('Latvian')),
|
||||
('nb-no', _('Norwegian Bokmål')),
|
||||
('pl', _('Polish')),
|
||||
('pt-pt', _('Portuguese (Portugal)')),
|
||||
('pt-br', _('Portuguese (Brazil)')),
|
||||
@@ -108,8 +109,9 @@ LANGUAGES_RTL = {
|
||||
'ar', 'hw'
|
||||
}
|
||||
LANGUAGES_INCUBATING = {
|
||||
'pl', 'fi', 'pt-br', 'gl',
|
||||
'fi', 'pt-br', 'gl',
|
||||
}
|
||||
LANGUAGES = ALL_LANGUAGES
|
||||
LOCALE_PATHS = [
|
||||
os.path.join(os.path.dirname(__file__), 'locale'),
|
||||
]
|
||||
@@ -233,7 +235,12 @@ COMPRESS_FILTERS = {
|
||||
)
|
||||
}
|
||||
|
||||
CURRENCIES = list(currencies)
|
||||
CURRENCIES = [
|
||||
c for c in currencies
|
||||
if c.alpha_3 not in {
|
||||
'XAG', 'XAU', 'XBA', 'XBB', 'XBC', 'XBD', 'XDR', 'XPD', 'XPT', 'XSU', 'XTS', 'XUA',
|
||||
}
|
||||
]
|
||||
CURRENCY_PLACES = {
|
||||
# default is 2
|
||||
'BIF': 0,
|
||||
@@ -266,9 +273,10 @@ CACHE_LARGE_VALUES_ALIAS = 'default'
|
||||
FILE_UPLOAD_EXTENSIONS_IMAGE = (".png", ".jpg", ".gif", ".jpeg")
|
||||
PILLOW_FORMATS_IMAGE = ('PNG', 'GIF', 'JPEG')
|
||||
|
||||
FILE_UPLOAD_EXTENSIONS_FAVICON = (".ico", ".png", "jpg", ".gif", ".jpeg")
|
||||
FILE_UPLOAD_EXTENSIONS_FAVICON = (".ico", ".png", ".jpg", ".gif", ".jpeg")
|
||||
PILLOW_FORMATS_QUESTIONS_FAVICON = ('PNG', 'GIF', 'JPEG', 'ICO')
|
||||
|
||||
FILE_UPLOAD_EXTENSIONS_QUESTION_IMAGE = (".png", "jpg", ".gif", ".jpeg", ".bmp", ".tif", ".tiff", ".jfif")
|
||||
FILE_UPLOAD_EXTENSIONS_QUESTION_IMAGE = (".png", ".jpg", ".gif", ".jpeg", ".bmp", ".tif", ".tiff", ".jfif")
|
||||
PILLOW_FORMATS_QUESTIONS_IMAGE = ('PNG', 'GIF', 'JPEG', 'BMP', 'TIFF')
|
||||
|
||||
FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT = (
|
||||
@@ -277,3 +285,5 @@ FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT = (
|
||||
".bmp", ".tif", ".tiff"
|
||||
)
|
||||
FILE_UPLOAD_EXTENSIONS_OTHER = FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT
|
||||
|
||||
PRETIX_MAX_ORDER_SIZE = 500
|
||||
|
||||
@@ -38,6 +38,7 @@ MAIL_FROM_ORGANIZERS = 'invalid@invalid'
|
||||
FILE_UPLOAD_MAX_SIZE_EMAIL_AUTO_ATTACHMENT = 10
|
||||
FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT = 10
|
||||
FILE_UPLOAD_MAX_SIZE_IMAGE = 10
|
||||
FILE_UPLOAD_MAX_SIZE_FAVICON = 10
|
||||
DEFAULT_CURRENCY = 'EUR'
|
||||
SECRET_KEY = "build-time-secret-key"
|
||||
HAS_REDIS = False
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import logging
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import exceptions
|
||||
@@ -29,6 +31,8 @@ from pretix.api.auth.devicesecurity import (
|
||||
)
|
||||
from pretix.base.models import Device
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DeviceTokenAuthentication(TokenAuthentication):
|
||||
model = Device
|
||||
@@ -46,6 +50,7 @@ class DeviceTokenAuthentication(TokenAuthentication):
|
||||
raise exceptions.AuthenticationFailed('Device has not been initialized.')
|
||||
|
||||
if device.revoked:
|
||||
logging.warning(f'Connection attempt of revoked device {device.pk}.')
|
||||
raise exceptions.AuthenticationFailed('Device access has been revoked.')
|
||||
|
||||
return AnonymousUser(), device
|
||||
|
||||
@@ -185,6 +185,7 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'api-v1:order-detail'),
|
||||
('DELETE', 'api-v1:orderposition-detail'),
|
||||
('PATCH', 'api-v1:orderposition-detail'),
|
||||
('GET', 'api-v1:orderposition-list'),
|
||||
('GET', 'api-v1:orderposition-answer'),
|
||||
('GET', 'api-v1:orderposition-pdf_image'),
|
||||
('POST', 'api-v1:order-mark-canceled'),
|
||||
@@ -223,6 +224,7 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
|
||||
('POST', 'api-v1:checkinrpc.redeem'),
|
||||
('GET', 'api-v1:checkinrpc.search'),
|
||||
('POST', 'api-v1:reusablemedium-lookup'),
|
||||
('GET', 'api-v1:reusablemedium-list'),
|
||||
('POST', 'api-v1:reusablemedium-list'),
|
||||
)
|
||||
|
||||
|
||||
@@ -39,7 +39,8 @@ from pretix.base.models import Device, Event, User
|
||||
from pretix.base.models.auth import SuperuserPermissionSet
|
||||
from pretix.base.models.organizer import TeamAPIToken
|
||||
from pretix.helpers.security import (
|
||||
SessionInvalid, SessionReauthRequired, assert_session_valid,
|
||||
Session2FASetupRequired, SessionInvalid, SessionPasswordChangeRequired,
|
||||
SessionReauthRequired, assert_session_valid,
|
||||
)
|
||||
|
||||
|
||||
@@ -66,6 +67,10 @@ class EventPermission(BasePermission):
|
||||
return False
|
||||
except SessionReauthRequired:
|
||||
return False
|
||||
except Session2FASetupRequired:
|
||||
return False
|
||||
except SessionPasswordChangeRequired:
|
||||
return False
|
||||
|
||||
perm_holder = (request.auth if isinstance(request.auth, (Device, TeamAPIToken))
|
||||
else request.user)
|
||||
@@ -144,6 +149,10 @@ class ProfilePermission(BasePermission):
|
||||
return False
|
||||
except SessionReauthRequired:
|
||||
return False
|
||||
except Session2FASetupRequired:
|
||||
return False
|
||||
except SessionPasswordChangeRequired:
|
||||
return False
|
||||
|
||||
if isinstance(request.auth, OAuthAccessToken):
|
||||
if not (request.auth.allow_scopes(['read']) or request.auth.allow_scopes(['profile'])) and request.method in SAFE_METHODS:
|
||||
@@ -166,5 +175,9 @@ class AnyAuthenticatedClientPermission(BasePermission):
|
||||
return False
|
||||
except SessionReauthRequired:
|
||||
return False
|
||||
except Session2FASetupRequired:
|
||||
return False
|
||||
except SessionPasswordChangeRequired:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
49
src/pretix/api/auth/session.py
Normal file
49
src/pretix/api/auth/session.py
Normal file
@@ -0,0 +1,49 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from rest_framework import exceptions
|
||||
from rest_framework.authentication import (
|
||||
SessionAuthentication as BaseSessionAuthentication,
|
||||
)
|
||||
|
||||
from pretix.multidomain.middlewares import CsrfViewMiddleware
|
||||
|
||||
|
||||
class CustomCSRFCheck(CsrfViewMiddleware):
|
||||
def _reject(self, request, reason):
|
||||
# Return the failure reason instead of an HttpResponse
|
||||
return reason
|
||||
|
||||
|
||||
class SessionAuthentication(BaseSessionAuthentication):
|
||||
# Override from DRF to user our custom CSRF middleware
|
||||
|
||||
def enforce_csrf(self, request):
|
||||
def dummy_get_response(request): # pragma: no cover
|
||||
return None
|
||||
|
||||
check = CustomCSRFCheck(dummy_get_response)
|
||||
# populates request.META['CSRF_COOKIE'], which is used in process_view()
|
||||
check.process_request(request)
|
||||
reason = check.process_view(request, None, (), {})
|
||||
if reason:
|
||||
# CSRF failed, bail with explicit error message
|
||||
raise exceptions.PermissionDenied('CSRF Failed: %s' % reason)
|
||||
@@ -54,7 +54,7 @@ class IdempotencyMiddleware:
|
||||
|
||||
auth_hash_parts = '{}:{}'.format(
|
||||
request.headers.get('Authorization', ''),
|
||||
request.COOKIES.get(settings.SESSION_COOKIE_NAME, '')
|
||||
request.COOKIES.get('__Host-' + settings.SESSION_COOKIE_NAME, request.COOKIES.get(settings.SESSION_COOKIE_NAME, ''))
|
||||
)
|
||||
auth_hash = sha1(auth_hash_parts.encode()).hexdigest()
|
||||
idempotency_key = request.headers.get('X-Idempotency-Key', '')
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.10 on 2024-02-12 11:52
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixapi", "0011_bigint"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="oauthapplication",
|
||||
name="post_logout_redirect_uris",
|
||||
field=models.TextField(default=""),
|
||||
),
|
||||
]
|
||||
@@ -42,6 +42,11 @@ class OAuthApplication(AbstractApplication):
|
||||
verbose_name=_("Redirection URIs"),
|
||||
help_text=_("Allowed URIs list, space separated")
|
||||
)
|
||||
post_logout_redirect_uris = models.TextField(
|
||||
blank=True, validators=[URIValidator],
|
||||
help_text=_("Allowed Post Logout URIs list, space separated"),
|
||||
default="",
|
||||
)
|
||||
client_id = models.CharField(
|
||||
verbose_name=_("Client ID"),
|
||||
max_length=100, unique=True, default=generate_client_id, db_index=True
|
||||
|
||||
@@ -38,7 +38,7 @@ class CheckinListSerializer(I18nAwareModelSerializer):
|
||||
model = CheckinList
|
||||
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count',
|
||||
'include_pending', 'auto_checkin_sales_channels', 'allow_multiple_entries', 'allow_entry_after_exit',
|
||||
'rules', 'exit_all_at', 'addon_match')
|
||||
'rules', 'exit_all_at', 'addon_match', 'ignore_in_statistics', 'consider_tickets_used')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -230,8 +230,8 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
for key, v in value['meta_data'].items():
|
||||
if key not in self.meta_properties:
|
||||
raise ValidationError(_('Meta data property \'{name}\' does not exist.').format(name=key))
|
||||
if self.meta_properties[key].allowed_values:
|
||||
if v not in [_v.strip() for _v in self.meta_properties[key].allowed_values.splitlines()]:
|
||||
if self.meta_properties[key].choices:
|
||||
if v not in self.meta_properties[key].choice_keys:
|
||||
raise ValidationError(_('Meta data property \'{name}\' does not allow value \'{value}\'.').format(name=key, value=v))
|
||||
return value
|
||||
|
||||
@@ -424,7 +424,7 @@ class CloneEventSerializer(EventSerializer):
|
||||
new_event = super().create({**validated_data, 'plugins': None})
|
||||
|
||||
event = Event.objects.filter(slug=self.context['event'], organizer=self.context['organizer'].pk).first()
|
||||
new_event.copy_data_from(event)
|
||||
new_event.copy_data_from(event, skip_meta_data='meta_data' in validated_data)
|
||||
|
||||
if plugins is not None:
|
||||
new_event.set_active_plugins(plugins)
|
||||
@@ -472,7 +472,8 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
||||
fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission',
|
||||
'presale_start', 'presale_end', 'location', 'geo_lat', 'geo_lon', 'event', 'is_public',
|
||||
'frontpage_text', 'seating_plan', 'item_price_overrides', 'variation_price_overrides',
|
||||
'meta_data', 'seat_category_mapping', 'last_modified', 'best_availability_state')
|
||||
'meta_data', 'seat_category_mapping', 'last_modified', 'best_availability_state',
|
||||
'comment')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -528,8 +529,8 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
||||
for key, v in value['meta_data'].items():
|
||||
if key not in self.meta_properties:
|
||||
raise ValidationError(_('Meta data property \'{name}\' does not exist.').format(name=key))
|
||||
if self.meta_properties[key].allowed_values:
|
||||
if v not in [_v.strip() for _v in self.meta_properties[key].allowed_values.splitlines()]:
|
||||
if self.meta_properties[key].choices:
|
||||
if v not in self.meta_properties[key].choice_keys:
|
||||
raise ValidationError(_('Meta data property \'{name}\' does not allow value \'{value}\'.').format(name=key, value=v))
|
||||
return value
|
||||
|
||||
@@ -687,6 +688,7 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'allow_modifications_after_checkin',
|
||||
'show_quota_left',
|
||||
'waiting_list_enabled',
|
||||
'waiting_list_auto_disable',
|
||||
'waiting_list_hours',
|
||||
'waiting_list_auto',
|
||||
'waiting_list_names_asked',
|
||||
@@ -705,6 +707,7 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'frontpage_subevent_ordering',
|
||||
'event_list_type',
|
||||
'event_list_available_only',
|
||||
'event_list_filters',
|
||||
'event_calendar_future_only',
|
||||
'frontpage_text',
|
||||
'event_info_text',
|
||||
@@ -795,6 +798,8 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'cancel_allow_user_paid_refund_as_giftcard',
|
||||
'cancel_allow_user_paid_require_approval',
|
||||
'cancel_allow_user_paid_require_approval_fee_unknown',
|
||||
'cancel_terms_paid',
|
||||
'cancel_terms_unpaid',
|
||||
'change_allow_user_variation',
|
||||
'change_allow_user_addons',
|
||||
'change_allow_user_until',
|
||||
|
||||
@@ -20,11 +20,14 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.http import QueryDict
|
||||
from pytz import common_timezones
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from pretix.base.exporter import OrganizerLevelExportMixin
|
||||
from pretix.base.models import ScheduledEventExport, ScheduledOrganizerExport
|
||||
from pretix.base.timeframes import DateFrameField, SerializerDateFrameField
|
||||
|
||||
|
||||
@@ -197,3 +200,92 @@ class JobRunSerializer(serializers.Serializer):
|
||||
raise ValidationError(self.errors)
|
||||
|
||||
return not bool(self._errors)
|
||||
|
||||
|
||||
class ScheduledExportSerializer(serializers.ModelSerializer):
|
||||
schedule_next_run = serializers.DateTimeField(read_only=True)
|
||||
export_identifier = serializers.ChoiceField(choices=[])
|
||||
locale = serializers.ChoiceField(choices=settings.LANGUAGES, default='en')
|
||||
owner = serializers.SlugRelatedField(slug_field='email', read_only=True)
|
||||
error_counter = serializers.IntegerField(read_only=True)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['export_identifier'].choices = [(e, e) for e in self.context['exporters']]
|
||||
|
||||
def validate(self, attrs):
|
||||
if attrs.get("export_form_data"):
|
||||
identifier = attrs.get('export_identifier', self.instance.export_identifier if self.instance else None)
|
||||
exporter = self.context['exporters'].get(identifier)
|
||||
if exporter:
|
||||
try:
|
||||
JobRunSerializer(exporter=exporter).to_internal_value(attrs["export_form_data"])
|
||||
except ValidationError as e:
|
||||
raise ValidationError({"export_form_data": e.detail})
|
||||
else:
|
||||
raise ValidationError({"export_identifier": ["Unknown exporter."]})
|
||||
return attrs
|
||||
|
||||
def validate_mail_additional_recipients(self, value):
|
||||
d = value.replace(' ', '')
|
||||
if len(d.split(',')) > 25:
|
||||
raise ValidationError('Please enter less than 25 recipients.')
|
||||
return d
|
||||
|
||||
def validate_mail_additional_recipients_cc(self, value):
|
||||
d = value.replace(' ', '')
|
||||
if len(d.split(',')) > 25:
|
||||
raise ValidationError('Please enter less than 25 recipients.')
|
||||
return d
|
||||
|
||||
def validate_mail_additional_recipients_bcc(self, value):
|
||||
d = value.replace(' ', '')
|
||||
if len(d.split(',')) > 25:
|
||||
raise ValidationError('Please enter less than 25 recipients.')
|
||||
return d
|
||||
|
||||
|
||||
class ScheduledEventExportSerializer(ScheduledExportSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ScheduledEventExport
|
||||
fields = [
|
||||
'id',
|
||||
'owner',
|
||||
'export_identifier',
|
||||
'export_form_data',
|
||||
'locale',
|
||||
'mail_additional_recipients',
|
||||
'mail_additional_recipients_cc',
|
||||
'mail_additional_recipients_bcc',
|
||||
'mail_subject',
|
||||
'mail_template',
|
||||
'schedule_rrule',
|
||||
'schedule_rrule_time',
|
||||
'schedule_next_run',
|
||||
'error_counter',
|
||||
]
|
||||
|
||||
|
||||
class ScheduledOrganizerExportSerializer(ScheduledExportSerializer):
|
||||
timezone = serializers.ChoiceField(default=settings.TIME_ZONE, choices=[(a, a) for a in common_timezones])
|
||||
|
||||
class Meta:
|
||||
model = ScheduledOrganizerExport
|
||||
fields = [
|
||||
'id',
|
||||
'owner',
|
||||
'export_identifier',
|
||||
'export_form_data',
|
||||
'locale',
|
||||
'mail_additional_recipients',
|
||||
'mail_additional_recipients_cc',
|
||||
'mail_additional_recipients_bcc',
|
||||
'mail_subject',
|
||||
'mail_template',
|
||||
'schedule_rrule',
|
||||
'schedule_rrule_time',
|
||||
'schedule_next_run',
|
||||
'timezone',
|
||||
'error_counter',
|
||||
]
|
||||
|
||||
@@ -59,9 +59,10 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = ItemVariation
|
||||
fields = ('id', 'value', 'active', 'description',
|
||||
'position', 'default_price', 'price', 'original_price', 'require_approval',
|
||||
'position', 'default_price', 'price', 'original_price', 'free_price_suggestion', 'require_approval',
|
||||
'require_membership', 'require_membership_types', 'require_membership_hidden',
|
||||
'checkin_attention', 'available_from', 'available_until',
|
||||
'checkin_attention', 'checkin_text',
|
||||
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
||||
'sales_channels', 'hide_without_voucher', 'meta_data')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -83,9 +84,10 @@ class ItemVariationSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = ItemVariation
|
||||
fields = ('id', 'value', 'active', 'description',
|
||||
'position', 'default_price', 'price', 'original_price', 'require_approval',
|
||||
'position', 'default_price', 'price', 'original_price', 'free_price_suggestion', 'require_approval',
|
||||
'require_membership', 'require_membership_types', 'require_membership_hidden',
|
||||
'checkin_attention', 'available_from', 'available_until',
|
||||
'checkin_attention', 'checkin_text',
|
||||
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
||||
'sales_channels', 'hide_without_voucher', 'meta_data')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -234,12 +236,14 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = Item
|
||||
fields = ('id', 'category', 'name', 'internal_name', 'active', 'sales_channels', 'description',
|
||||
'default_price', 'free_price', 'tax_rate', 'tax_rule', 'admission', 'personalized',
|
||||
'position', 'picture', 'available_from', 'available_until',
|
||||
'default_price', 'free_price', 'free_price_suggestion', 'tax_rate', 'tax_rule', 'admission',
|
||||
'personalized', 'position', 'picture',
|
||||
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
||||
'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', 'checkin_text', 'has_variations', 'variations',
|
||||
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
|
||||
'show_quota_left', 'hidden_if_available', 'allow_waitinglist', 'issue_giftcard', 'meta_data',
|
||||
'show_quota_left', 'hidden_if_available', 'hidden_if_item_available', 'allow_waitinglist',
|
||||
'issue_giftcard', 'meta_data',
|
||||
'require_membership', 'require_membership_types', 'require_membership_hidden', 'grant_membership_type',
|
||||
'grant_membership_duration_like_event', 'grant_membership_duration_days',
|
||||
'grant_membership_duration_months', 'validity_mode', 'validity_fixed_from', 'validity_fixed_until',
|
||||
@@ -439,7 +443,7 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = Question
|
||||
fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position',
|
||||
'ask_during_checkin', 'identifier', 'dependency_question', 'dependency_values',
|
||||
'ask_during_checkin', 'show_during_checkin', 'identifier', 'dependency_question', 'dependency_values',
|
||||
'hidden', 'dependency_value', 'print_on_invoice', 'help_text', 'valid_number_min',
|
||||
'valid_number_max', 'valid_date_min', 'valid_date_max', 'valid_datetime_min', 'valid_datetime_max',
|
||||
'valid_string_length_max', 'valid_file_portrait')
|
||||
@@ -485,6 +489,9 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
||||
if full_data.get('ask_during_checkin') and full_data.get('type') in Question.ASK_DURING_CHECKIN_UNSUPPORTED:
|
||||
raise ValidationError(_('This type of question cannot be asked during check-in.'))
|
||||
|
||||
if full_data.get('show_during_checkin') and full_data.get('type') in Question.SHOW_DURING_CHECKIN_UNSUPPORTED:
|
||||
raise ValidationError(_('This type of question cannot be shown during check-in.'))
|
||||
|
||||
Question.clean_items(event, full_data.get('items'))
|
||||
return data
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ from pretix.api.serializers import CompatibleJSONField
|
||||
from pretix.api.serializers.event import SubEventSerializer
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.item import (
|
||||
InlineItemVariationSerializer, ItemSerializer,
|
||||
InlineItemVariationSerializer, ItemSerializer, QuestionSerializer,
|
||||
)
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.decimal import round_decimal
|
||||
@@ -486,11 +486,11 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||
'company', 'street', 'zipcode', 'city', 'country', 'state', 'discount',
|
||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled',
|
||||
'valid_from', 'valid_until', 'blocked')
|
||||
'valid_from', 'valid_until', 'blocked', 'voucher_budget_use')
|
||||
read_only_fields = (
|
||||
'id', 'order', 'positionid', 'item', 'variation', 'price', 'voucher', 'tax_rate', 'tax_value', 'secret',
|
||||
'addon_to', 'subevent', 'checkins', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data',
|
||||
'seat', 'canceled', 'discount', 'valid_from', 'valid_until', 'blocked'
|
||||
'seat', 'canceled', 'discount', 'valid_from', 'valid_until', 'blocked', 'voucher_budget_use'
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -501,7 +501,7 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||
# /events/…/checkinlists/…/positions/
|
||||
# We're unable to check this on this level if we're on /checkinrpc/, in which case we rely on the view
|
||||
# layer to not set pdf_data=true in the first place.
|
||||
request and hasattr(request, 'event') and 'can_view_orders' not in request.eventpermset
|
||||
request and hasattr(request, 'eventpermset') and 'can_view_orders' not in request.eventpermset
|
||||
)
|
||||
if ('pdf_data' in self.context and not self.context['pdf_data']) or pdf_data_forbidden:
|
||||
self.fields.pop('pdf_data', None)
|
||||
@@ -585,6 +585,9 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
||||
if 'variation' in self.context['expand']:
|
||||
self.fields['variation'] = InlineItemVariationSerializer(read_only=True)
|
||||
|
||||
if 'answers.question' in self.context['expand']:
|
||||
self.fields['answers'].child.fields['question'] = QuestionSerializer(read_only=True)
|
||||
|
||||
|
||||
class OrderPaymentTypeField(serializers.Field):
|
||||
# TODO: Remove after pretix 2.2
|
||||
@@ -715,7 +718,7 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
fields = (
|
||||
'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
|
||||
'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads',
|
||||
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
|
||||
'checkin_attention', 'checkin_text', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
|
||||
'url', 'customer', 'valid_if_pending'
|
||||
)
|
||||
read_only_fields = (
|
||||
@@ -771,8 +774,8 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
def update(self, instance, validated_data):
|
||||
# Even though all fields that shouldn't be edited are marked as read_only in the serializer
|
||||
# (hopefully), we'll be extra careful here and be explicit about the model fields we update.
|
||||
update_fields = ['comment', 'custom_followup_at', 'checkin_attention', 'email', 'locale', 'phone',
|
||||
'valid_if_pending']
|
||||
update_fields = ['comment', 'custom_followup_at', 'checkin_attention', 'checkin_text', 'email', 'locale',
|
||||
'phone', 'valid_if_pending']
|
||||
|
||||
if 'invoice_address' in validated_data:
|
||||
iadata = validated_data.pop('invoice_address')
|
||||
@@ -1032,13 +1035,14 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['positions'].child.fields['voucher'].queryset = self.context['event'].vouchers.all()
|
||||
self.fields['customer'].queryset = self.context['event'].organizer.customers.all()
|
||||
self.fields['expires'].required = False
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = ('code', 'status', 'testmode', 'email', 'phone', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
|
||||
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts',
|
||||
'force', 'send_email', 'simulate', 'customer', 'custom_followup_at', 'require_approval',
|
||||
'valid_if_pending')
|
||||
'invoice_address', 'positions', 'checkin_attention', 'checkin_text', 'payment_info', 'payment_date',
|
||||
'consume_carts', 'force', 'send_email', 'simulate', 'customer', 'custom_followup_at',
|
||||
'require_approval', 'valid_if_pending', 'expires')
|
||||
|
||||
def validate_payment_provider(self, pp):
|
||||
if pp is None:
|
||||
@@ -1047,6 +1051,11 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
raise ValidationError('The given payment provider is not known.')
|
||||
return pp
|
||||
|
||||
def validate_expires(self, expires):
|
||||
if expires < now():
|
||||
raise ValidationError('Expiration date must be in the future.')
|
||||
return expires
|
||||
|
||||
def validate_sales_channel(self, channel):
|
||||
if channel not in get_all_sales_channels():
|
||||
raise ValidationError('Unknown sales channel.')
|
||||
@@ -1068,6 +1077,10 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
raise ValidationError(
|
||||
'An order cannot be empty.'
|
||||
)
|
||||
if len(data) > settings.PRETIX_MAX_ORDER_SIZE:
|
||||
raise ValidationError(
|
||||
'Orders cannot have more than %(max)s positions.' % {'max': settings.PRETIX_MAX_ORDER_SIZE}
|
||||
)
|
||||
errs = [{} for p in data]
|
||||
if any([p.get('positionid') for p in data]):
|
||||
if not all([p.get('positionid') for p in data]):
|
||||
@@ -1302,7 +1315,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
if 'valid_from' not in pos_data and 'valid_until' not in pos_data:
|
||||
valid_from, valid_until = pos_data['item'].compute_validity(
|
||||
requested_start=(
|
||||
max(requested_valid_from, now())
|
||||
requested_valid_from
|
||||
if requested_valid_from and pos_data['item'].validity_dynamic_start_choice
|
||||
else now()
|
||||
),
|
||||
@@ -1353,7 +1366,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
if validated_data.get('locale', None) is None:
|
||||
validated_data['locale'] = self.context['event'].settings.locale
|
||||
order = Order(event=self.context['event'], **validated_data)
|
||||
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
|
||||
if not validated_data.get('expires'):
|
||||
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
|
||||
order.meta_info = "{}"
|
||||
order.total = Decimal('0.00')
|
||||
if validated_data.get('require_approval') is not None:
|
||||
@@ -1425,6 +1439,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
if not pos.item.tax_rule or pos.item.tax_rule.price_includes_tax:
|
||||
price_after_voucher = max(pos.price, pos.voucher.calculate_price(listed_price))
|
||||
else:
|
||||
pos._calculate_tax(invoice_address=ia)
|
||||
price_after_voucher = max(pos.price - pos.tax_value, pos.voucher.calculate_price(listed_price))
|
||||
else:
|
||||
price_after_voucher = listed_price
|
||||
@@ -1452,7 +1467,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
answers_data = pos_data.pop('answers', [])
|
||||
use_reusable_medium = pos_data.pop('use_reusable_medium', None)
|
||||
pos = pos_data['__instance']
|
||||
pos._calculate_tax()
|
||||
pos._calculate_tax(invoice_address=ia)
|
||||
|
||||
if simulate:
|
||||
pos = WrappedModel(pos)
|
||||
@@ -1571,7 +1586,10 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
if order.total == Decimal('0.00') and validated_data.get('status') == Order.STATUS_PAID and not payment_provider:
|
||||
payment_provider = 'free'
|
||||
|
||||
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID:
|
||||
if order.total != Decimal('0.00') and order.event.currency == "XXX":
|
||||
raise ValidationError('Paid products not supported without a valid currency.')
|
||||
|
||||
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID and not validated_data.get('require_approval'):
|
||||
order.status = Order.STATUS_PAID
|
||||
order.save()
|
||||
order.payments.create(
|
||||
@@ -1583,6 +1601,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
elif validated_data.get('status') == Order.STATUS_PAID:
|
||||
if not payment_provider:
|
||||
raise ValidationError('You cannot create a paid order without a payment provider.')
|
||||
if validated_data.get('require_approval'):
|
||||
raise ValidationError('You cannot create a paid order that requires approval.')
|
||||
order.payments.create(
|
||||
amount=order.total,
|
||||
provider=payment_provider,
|
||||
|
||||
@@ -79,8 +79,8 @@ class CustomerSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Customer
|
||||
fields = ('identifier', 'external_identifier', 'email', 'name', 'name_parts', 'is_active', 'is_verified', 'last_login', 'date_joined',
|
||||
'locale', 'last_modified', 'notes')
|
||||
fields = ('identifier', 'external_identifier', 'email', 'phone', 'name', 'name_parts', 'is_active',
|
||||
'is_verified', 'last_login', 'date_joined', 'locale', 'last_modified', 'notes')
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
if instance and instance.provider_id:
|
||||
@@ -239,7 +239,7 @@ class TeamSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Team
|
||||
fields = (
|
||||
'id', 'name', 'all_events', 'limit_events', 'can_create_events', 'can_change_teams',
|
||||
'id', 'name', 'require_2fa', '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', 'can_checkin_orders', 'can_manage_customers', 'can_manage_reusable_media'
|
||||
|
||||
@@ -89,6 +89,7 @@ class SettingsSerializer(serializers.Serializer):
|
||||
except OSError: # pragma: no cover
|
||||
logger.error('Deleting file %s failed.' % fname.name)
|
||||
instance.delete(attr)
|
||||
self.changed_data.append(attr)
|
||||
else:
|
||||
# file is unchanged
|
||||
continue
|
||||
|
||||
@@ -63,6 +63,7 @@ orga_router.register(r'teams', organizer.TeamViewSet)
|
||||
orga_router.register(r'devices', organizer.DeviceViewSet)
|
||||
orga_router.register(r'orders', order.OrganizerOrderViewSet)
|
||||
orga_router.register(r'invoices', order.InvoiceViewSet)
|
||||
orga_router.register(r'scheduled_exports', exporters.ScheduledOrganizerExportViewSet)
|
||||
orga_router.register(r'exporters', exporters.OrganizerExportersViewSet, basename='exporters')
|
||||
|
||||
team_router = routers.DefaultRouter()
|
||||
@@ -88,6 +89,7 @@ event_router.register(r'taxrules', event.TaxRuleViewSet)
|
||||
event_router.register(r'waitinglistentries', waitinglist.WaitingListViewSet)
|
||||
event_router.register(r'checkinlists', checkin.CheckinListViewSet)
|
||||
event_router.register(r'cartpositions', cart.CartPositionViewSet)
|
||||
event_router.register(r'scheduled_exports', exporters.ScheduledEventExportViewSet)
|
||||
event_router.register(r'exporters', exporters.EventExportersViewSet, basename='exporters')
|
||||
event_router.register(r'shredders', shredders.EventShreddersViewSet, basename='shredders')
|
||||
event_router.register(r'item_meta_properties', event.ItemMetaPropertiesViewSet)
|
||||
|
||||
@@ -35,6 +35,7 @@ from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from packaging.version import parse
|
||||
@@ -285,6 +286,8 @@ with scopes_disabled():
|
||||
return queryset.filter(last_checked_in__isnull=not value)
|
||||
|
||||
def check_rules_qs(self, queryset, name, value):
|
||||
if not value:
|
||||
return queryset
|
||||
if not self.checkinlist.rules:
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
@@ -536,6 +539,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
'reason': Checkin.REASON_ALREADY_REDEEMED,
|
||||
'reason_explanation': None,
|
||||
'require_attention': False,
|
||||
'checkin_texts': [],
|
||||
'__warning': 'Compatibility hack active due to detected old pretixSCAN version',
|
||||
}, status=400)
|
||||
except: # we don't care e.g. about invalid version numbers
|
||||
@@ -547,6 +551,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
'reason': Checkin.REASON_INVALID,
|
||||
'reason_explanation': None,
|
||||
'require_attention': False,
|
||||
'checkin_texts': [],
|
||||
'list': MiniCheckinListSerializer(checkinlists[0]).data,
|
||||
}, status=404)
|
||||
elif revoked_matches and force:
|
||||
@@ -576,11 +581,38 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
'reason': Checkin.REASON_REVOKED,
|
||||
'reason_explanation': None,
|
||||
'require_attention': False,
|
||||
'checkin_texts': [],
|
||||
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, revoked_matches[
|
||||
0].event)).data,
|
||||
'list': MiniCheckinListSerializer(list_by_event[revoked_matches[0].event_id]).data,
|
||||
}, status=400)
|
||||
else:
|
||||
if media.linked_orderposition.order.event_id not in list_by_event:
|
||||
# Medium exists but connected ticket is for the wrong event
|
||||
if not simulate:
|
||||
checkinlists[0].event.log_action('pretix.event.checkin.unknown', data={
|
||||
'datetime': datetime,
|
||||
'type': checkin_type,
|
||||
'list': checkinlists[0].pk,
|
||||
'barcode': raw_barcode,
|
||||
'searched_lists': [cl.pk for cl in checkinlists]
|
||||
}, user=user, auth=auth)
|
||||
Checkin.objects.create(
|
||||
position=None,
|
||||
successful=False,
|
||||
error_reason=Checkin.REASON_INVALID,
|
||||
error_explanation=gettext('Medium connected to other event'),
|
||||
**common_checkin_args,
|
||||
)
|
||||
return Response({
|
||||
'detail': 'Not found.', # for backwards compatibility
|
||||
'status': 'error',
|
||||
'reason': Checkin.REASON_INVALID,
|
||||
'reason_explanation': gettext('Medium connected to other event'),
|
||||
'require_attention': False,
|
||||
'checkin_texts': [],
|
||||
'list': MiniCheckinListSerializer(checkinlists[0]).data,
|
||||
}, status=404)
|
||||
op_candidates = [media.linked_orderposition]
|
||||
if list_by_event[media.linked_orderposition.order.event_id].addon_match:
|
||||
op_candidates += list(media.linked_orderposition.addons.all())
|
||||
@@ -631,6 +663,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
'reason': Checkin.REASON_AMBIGUOUS,
|
||||
'reason_explanation': None,
|
||||
'require_attention': op.require_checkin_attention,
|
||||
'checkin_texts': op.checkin_texts,
|
||||
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, op.order.event)).data,
|
||||
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
|
||||
}, status=400)
|
||||
@@ -679,6 +712,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
return Response({
|
||||
'status': 'incomplete',
|
||||
'require_attention': op.require_checkin_attention,
|
||||
'checkin_texts': op.checkin_texts,
|
||||
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, op.order.event)).data,
|
||||
'questions': [
|
||||
QuestionSerializer(q).data for q in e.questions
|
||||
@@ -709,6 +743,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
'reason': e.code,
|
||||
'reason_explanation': e.reason,
|
||||
'require_attention': op.require_checkin_attention,
|
||||
'checkin_texts': op.checkin_texts,
|
||||
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, op.order.event)).data,
|
||||
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
|
||||
}, status=400)
|
||||
@@ -716,6 +751,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
return Response({
|
||||
'status': 'ok',
|
||||
'require_attention': op.require_checkin_attention,
|
||||
'checkin_texts': op.checkin_texts,
|
||||
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, op.order.event)).data,
|
||||
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
|
||||
}, status=201)
|
||||
|
||||
@@ -89,6 +89,8 @@ class UpdateRequestSerializer(serializers.Serializer):
|
||||
|
||||
class RSAEncryptedField(serializers.Field):
|
||||
def to_representation(self, value):
|
||||
if isinstance(value, memoryview):
|
||||
value = value.tobytes()
|
||||
public_key = load_pem_public_key(
|
||||
self.context['device'].rsa_pubkey.encode(), Backend()
|
||||
)
|
||||
|
||||
@@ -190,7 +190,10 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
original_data = self.get_serializer(instance=serializer.instance).data
|
||||
|
||||
current_live_value = serializer.instance.live
|
||||
updated_live_value = serializer.validated_data.get('live', None)
|
||||
current_plugins_value = serializer.instance.get_plugins()
|
||||
@@ -198,6 +201,11 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
|
||||
super().perform_update(serializer)
|
||||
|
||||
if serializer.data == original_data:
|
||||
# Performance optimization: If nothing was changed, we do not need to save or log anything.
|
||||
# This costs us a few cycles on save, but avoids thousands of lines in our log.
|
||||
return
|
||||
|
||||
if updated_live_value is not None and updated_live_value != current_live_value:
|
||||
log_action = 'pretix.event.live.activated' if updated_live_value else 'pretix.event.live.deactivated'
|
||||
serializer.instance.log_action(
|
||||
@@ -254,7 +262,7 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
new_event = serializer.save(organizer=self.request.organizer)
|
||||
|
||||
if copy_from:
|
||||
new_event.copy_data_from(copy_from)
|
||||
new_event.copy_data_from(copy_from, skip_meta_data='meta_data' in serializer.validated_data)
|
||||
|
||||
if plugins is not None:
|
||||
new_event.set_active_plugins(plugins)
|
||||
@@ -291,7 +299,7 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
try:
|
||||
with transaction.atomic():
|
||||
instance.organizer.log_action(
|
||||
'pretix.event.deleted', user=self.request.user,
|
||||
'pretix.event.deleted', user=self.request.user, auth=self.request.auth,
|
||||
data={
|
||||
'event_id': instance.pk,
|
||||
'name': str(instance.name),
|
||||
@@ -622,11 +630,12 @@ class EventSettingsView(views.APIView):
|
||||
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()
|
||||
}
|
||||
)
|
||||
if s.changed_data:
|
||||
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()
|
||||
}
|
||||
)
|
||||
if any(p in s.changed_data for p in SETTINGS_AFFECTING_CSS):
|
||||
regenerate_css.apply_async(args=(request.event.pk,))
|
||||
s = EventSettingsSerializer(
|
||||
|
||||
@@ -29,14 +29,20 @@ from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from pretix.api.pagination import TotalOrderingFilter
|
||||
from pretix.api.serializers.exporters import (
|
||||
ExporterSerializer, JobRunSerializer,
|
||||
ExporterSerializer, JobRunSerializer, ScheduledEventExportSerializer,
|
||||
ScheduledOrganizerExportSerializer,
|
||||
)
|
||||
from pretix.base.exporter import OrganizerLevelExportMixin
|
||||
from pretix.base.models import CachedFile, Device, Event, TeamAPIToken
|
||||
from pretix.base.models import (
|
||||
CachedFile, Device, Event, ScheduledEventExport, ScheduledOrganizerExport,
|
||||
TeamAPIToken,
|
||||
)
|
||||
from pretix.base.services.export import export, multiexport
|
||||
from pretix.base.signals import (
|
||||
register_data_exporters, register_multievent_data_exporters,
|
||||
@@ -199,3 +205,152 @@ class OrganizerExportersViewSet(ExportersMixin, viewsets.ViewSet):
|
||||
'provider': instance.identifier,
|
||||
'form_data': data
|
||||
})
|
||||
|
||||
|
||||
class ScheduledExportersViewSet(viewsets.ModelViewSet):
|
||||
filter_backends = (TotalOrderingFilter,)
|
||||
ordering = ('id',)
|
||||
ordering_fields = ('id', 'export_identifier', 'schedule_next_run')
|
||||
|
||||
|
||||
class ScheduledEventExportViewSet(ScheduledExportersViewSet):
|
||||
serializer_class = ScheduledEventExportSerializer
|
||||
queryset = ScheduledEventExport.objects.none()
|
||||
permission = 'can_view_orders'
|
||||
|
||||
def get_queryset(self):
|
||||
perm_holder = self.request.auth if isinstance(self.request.auth, (TeamAPIToken, Device)) else self.request.user
|
||||
if not perm_holder.has_event_permission(self.request.organizer, self.request.event, 'can_change_event_settings',
|
||||
request=self.request):
|
||||
if self.request.user.is_authenticated:
|
||||
qs = self.request.event.scheduled_exports.filter(owner=self.request.user)
|
||||
else:
|
||||
raise PermissionDenied('Scheduled exports require either permission to change event settings or '
|
||||
'user-specific API access.')
|
||||
else:
|
||||
qs = self.request.event.scheduled_exports
|
||||
return qs.select_related("owner")
|
||||
|
||||
def perform_create(self, serializer):
|
||||
if not self.request.user.is_authenticated:
|
||||
raise PermissionDenied('Creation of exports requires user-specific API access.')
|
||||
serializer.save(event=self.request.event, owner=self.request.user)
|
||||
serializer.instance.compute_next_run()
|
||||
serializer.instance.save(update_fields=["schedule_next_run"])
|
||||
self.request.event.log_action(
|
||||
'pretix.event.export.schedule.added',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['event'] = self.request.event
|
||||
ctx['exporters'] = self.exporters
|
||||
return ctx
|
||||
|
||||
@cached_property
|
||||
def exporters(self):
|
||||
responses = register_data_exporters.send(self.request.event)
|
||||
exporters = [response(self.request.event, self.request.organizer) for r, response in responses if response]
|
||||
return {e.identifier: e for e in exporters}
|
||||
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.compute_next_run()
|
||||
serializer.instance.error_counter = 0
|
||||
serializer.instance.error_last_message = None
|
||||
serializer.instance.save(update_fields=["schedule_next_run", "error_counter", "error_last_message"])
|
||||
self.request.event.log_action(
|
||||
'pretix.event.export.schedule.changed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
self.request.event.log_action(
|
||||
'pretix.event.export.schedule.deleted',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
)
|
||||
super().perform_destroy(instance)
|
||||
|
||||
|
||||
class ScheduledOrganizerExportViewSet(ScheduledExportersViewSet):
|
||||
serializer_class = ScheduledOrganizerExportSerializer
|
||||
queryset = ScheduledOrganizerExport.objects.none()
|
||||
permission = None
|
||||
|
||||
def get_queryset(self):
|
||||
perm_holder = self.request.auth if isinstance(self.request.auth, (TeamAPIToken, Device)) else self.request.user
|
||||
if not perm_holder.has_organizer_permission(self.request.organizer, 'can_change_organizer_settings',
|
||||
request=self.request):
|
||||
if self.request.user.is_authenticated:
|
||||
qs = self.request.organizer.scheduled_exports.filter(owner=self.request.user)
|
||||
else:
|
||||
raise PermissionDenied('Scheduled exports require either permission to change organizer settings or '
|
||||
'user-specific API access.')
|
||||
else:
|
||||
qs = self.request.organizer.scheduled_exports
|
||||
return qs.select_related("owner")
|
||||
|
||||
def perform_create(self, serializer):
|
||||
if not self.request.user.is_authenticated:
|
||||
raise PermissionDenied('Creation of exports requires user-specific API access.')
|
||||
serializer.save(organizer=self.request.organizer, owner=self.request.user)
|
||||
serializer.instance.compute_next_run()
|
||||
serializer.instance.save(update_fields=["schedule_next_run"])
|
||||
self.request.organizer.log_action(
|
||||
'pretix.organizer.export.schedule.added',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['organizer'] = self.request.organizer
|
||||
ctx['exporters'] = self.exporters
|
||||
return ctx
|
||||
|
||||
@cached_property
|
||||
def events(self):
|
||||
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
||||
return self.request.auth.get_events_with_permission('can_view_orders')
|
||||
elif self.request.user.is_authenticated:
|
||||
return self.request.user.get_events_with_permission('can_view_orders', self.request).filter(
|
||||
organizer=self.request.organizer
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def exporters(self):
|
||||
responses = register_multievent_data_exporters.send(self.request.organizer)
|
||||
exporters = [
|
||||
response(Event.objects.none() if issubclass(response, OrganizerLevelExportMixin) else self.events,
|
||||
self.request.organizer)
|
||||
for r, response in responses if response
|
||||
]
|
||||
return {e.identifier: e for e in exporters}
|
||||
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(organizer=self.request.organizer)
|
||||
serializer.instance.compute_next_run()
|
||||
serializer.instance.error_counter = 0
|
||||
serializer.instance.error_last_message = None
|
||||
serializer.instance.save(update_fields=["schedule_next_run", "error_counter", "error_last_message"])
|
||||
self.request.organizer.log_action(
|
||||
'pretix.organizer.export.schedule.changed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
self.request.organizer.log_action(
|
||||
'pretix.organizer.export.schedule.deleted',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
)
|
||||
super().perform_destroy(instance)
|
||||
|
||||
@@ -42,7 +42,7 @@ class IdempotencyQueryView(APIView):
|
||||
idempotency_key = request.GET.get("key")
|
||||
auth_hash_parts = '{}:{}'.format(
|
||||
request.headers.get('Authorization', ''),
|
||||
request.COOKIES.get(settings.SESSION_COOKIE_NAME, '')
|
||||
request.COOKIES.get('__Host-' + settings.SESSION_COOKIE_NAME, request.COOKIES.get(settings.SESSION_COOKIE_NAME, ''))
|
||||
)
|
||||
auth_hash = sha1(auth_hash_parts.encode()).hexdigest()
|
||||
if not idempotency_key:
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import datetime
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
from decimal import Decimal
|
||||
@@ -27,7 +28,7 @@ from zoneinfo import ZoneInfo
|
||||
|
||||
import django_filters
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.db import IntegrityError, transaction
|
||||
from django.db.models import (
|
||||
Exists, F, OuterRef, Prefetch, Q, Subquery, prefetch_related_objects,
|
||||
)
|
||||
@@ -96,6 +97,9 @@ from pretix.base.signals import (
|
||||
)
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.control.signals import order_search_filter_q
|
||||
from pretix.helpers import OF_SELF
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
with scopes_disabled():
|
||||
class OrderFilter(FilterSet):
|
||||
@@ -110,10 +114,11 @@ with scopes_disabled():
|
||||
item = django_filters.CharFilter(field_name='all_positions', lookup_expr='item_id', distinct=True)
|
||||
variation = django_filters.CharFilter(field_name='all_positions', lookup_expr='variation_id', distinct=True)
|
||||
subevent = django_filters.CharFilter(field_name='all_positions', lookup_expr='subevent_id', distinct=True)
|
||||
customer = django_filters.CharFilter(field_name='customer__identifier')
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = ['code', 'status', 'email', 'locale', 'testmode', 'require_approval']
|
||||
fields = ['code', 'status', 'email', 'locale', 'testmode', 'require_approval', 'customer']
|
||||
|
||||
@scopes_disabled()
|
||||
def subevent_after_qs(self, qs, name, value):
|
||||
@@ -221,6 +226,8 @@ class OrderViewSetMixin:
|
||||
qs = qs.prefetch_related('refunds', 'refunds__payment')
|
||||
if 'invoice_address' not in self.request.GET.getlist('exclude'):
|
||||
qs = qs.select_related('invoice_address')
|
||||
if 'customer' not in self.request.GET.getlist('exclude'):
|
||||
qs = qs.select_related('customer')
|
||||
|
||||
qs = qs.prefetch_related(self._positions_prefetch(self.request))
|
||||
return qs
|
||||
@@ -569,8 +576,10 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
@action(detail=True, methods=['POST'])
|
||||
@transaction.atomic()
|
||||
def create_invoice(self, request, **kwargs):
|
||||
order = self.get_object()
|
||||
order = Order.objects.select_for_update(of=OF_SELF).get(pk=order.pk)
|
||||
has_inv = order.invoices.exists() and not (
|
||||
order.status in (Order.STATUS_PAID, Order.STATUS_PENDING)
|
||||
and order.invoices.filter(is_cancellation=True).count() >= order.invoices.filter(is_cancellation=False).count()
|
||||
@@ -826,6 +835,16 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
||||
}
|
||||
)
|
||||
|
||||
if 'checkin_text' in self.request.data and serializer.instance.checkin_text != self.request.data.get('checkin_text'):
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.order.checkin_text',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={
|
||||
'new_value': self.request.data.get('checkin_text')
|
||||
}
|
||||
)
|
||||
|
||||
if 'valid_if_pending' in self.request.data and serializer.instance.valid_if_pending != self.request.data.get('valid_if_pending'):
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.order.valid_if_pending',
|
||||
@@ -887,7 +906,11 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
||||
order_modified.send(sender=serializer.instance.event, order=serializer.instance)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save()
|
||||
try:
|
||||
serializer.save()
|
||||
except IntegrityError:
|
||||
logger.exception("Integrity error while saving order")
|
||||
raise ValidationError("Integrity error, possibly duplicate submission of same order.")
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
if not instance.testmode:
|
||||
@@ -1885,6 +1908,7 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
return Response(status=204)
|
||||
|
||||
@action(detail=True, methods=['POST'])
|
||||
@transaction.atomic()
|
||||
def reissue(self, request, **kwargs):
|
||||
inv = self.get_object()
|
||||
if inv.canceled:
|
||||
@@ -1892,9 +1916,10 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
elif inv.shredded:
|
||||
raise PermissionDenied('The invoice file is no longer stored on the server.')
|
||||
else:
|
||||
order = Order.objects.select_for_update(of=OF_SELF).get(pk=inv.order_id)
|
||||
c = generate_cancellation(inv)
|
||||
if inv.order.status != Order.STATUS_CANCELED:
|
||||
inv = generate_invoice(inv.order)
|
||||
inv = generate_invoice(order)
|
||||
else:
|
||||
inv = c
|
||||
inv.order.log_action(
|
||||
|
||||
@@ -24,6 +24,8 @@ from decimal import Decimal
|
||||
import django_filters
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.db import transaction
|
||||
from django.db.models import OuterRef, Subquery, Sum
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.functional import cached_property
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
@@ -155,8 +157,13 @@ class GiftCardViewSet(viewsets.ModelViewSet):
|
||||
qs = self.request.organizer.accepted_gift_cards
|
||||
else:
|
||||
qs = self.request.organizer.issued_gift_cards.all()
|
||||
s = GiftCardTransaction.objects.filter(
|
||||
card=OuterRef('pk')
|
||||
).order_by().values('card').annotate(s=Sum('value')).values('s')
|
||||
return qs.prefetch_related(
|
||||
'issuer'
|
||||
).annotate(
|
||||
cached_value=Coalesce(Subquery(s), Decimal('0.00'))
|
||||
)
|
||||
|
||||
def get_serializer_context(self):
|
||||
|
||||
@@ -176,7 +176,7 @@ class ParametrizedItemWebhookEvent(ParametrizedWebhookEvent):
|
||||
}
|
||||
|
||||
|
||||
class ParametrizedOrderPositionWebhookEvent(ParametrizedOrderWebhookEvent):
|
||||
class ParametrizedOrderPositionCheckinWebhookEvent(ParametrizedOrderWebhookEvent):
|
||||
|
||||
def build_payload(self, logentry: LogEntry):
|
||||
d = super().build_payload(logentry)
|
||||
@@ -185,6 +185,7 @@ class ParametrizedOrderPositionWebhookEvent(ParametrizedOrderWebhookEvent):
|
||||
d['orderposition_id'] = logentry.parsed_data.get('position')
|
||||
d['orderposition_positionid'] = logentry.parsed_data.get('positionid')
|
||||
d['checkin_list'] = logentry.parsed_data.get('list')
|
||||
d['type'] = logentry.parsed_data.get('type')
|
||||
d['first_checkin'] = logentry.parsed_data.get('first_checkin')
|
||||
return d
|
||||
|
||||
@@ -296,11 +297,11 @@ def register_default_webhook_events(sender, **kwargs):
|
||||
'pretix.event.order.denied',
|
||||
_('Order denied'),
|
||||
),
|
||||
ParametrizedOrderPositionWebhookEvent(
|
||||
ParametrizedOrderPositionCheckinWebhookEvent(
|
||||
'pretix.event.checkin',
|
||||
_('Ticket checked in'),
|
||||
),
|
||||
ParametrizedOrderPositionWebhookEvent(
|
||||
ParametrizedOrderPositionCheckinWebhookEvent(
|
||||
'pretix.event.checkin.reverted',
|
||||
_('Ticket check-in reverted'),
|
||||
),
|
||||
@@ -384,7 +385,7 @@ def register_default_webhook_events(sender, **kwargs):
|
||||
def notify_webhooks(logentry_ids: list):
|
||||
if not isinstance(logentry_ids, list):
|
||||
logentry_ids = [logentry_ids]
|
||||
qs = LogEntry.all.select_related('event', 'event__organizer').filter(id__in=logentry_ids)
|
||||
qs = LogEntry.all.select_related('event', 'event__organizer', 'organizer').filter(id__in=logentry_ids)
|
||||
_org, _at, webhooks = None, None, None
|
||||
for logentry in qs:
|
||||
if not logentry.organizer:
|
||||
|
||||
@@ -46,7 +46,7 @@ class PretixBaseConfig(AppConfig):
|
||||
from . import invoice # NOQA
|
||||
from . import notifications # NOQA
|
||||
from . import email # NOQA
|
||||
from .services import auth, checkin, currencies, export, mail, tickets, cart, orderimport, orders, invoices, cleanup, update_check, quotas, notifications, vouchers # NOQA
|
||||
from .services import auth, checkin, currencies, export, mail, tickets, cart, modelimport, orders, invoices, cleanup, update_check, quotas, notifications, vouchers # NOQA
|
||||
from .models import _transactions # NOQA
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
@@ -103,15 +103,17 @@ def get_all_sales_channels():
|
||||
if _ALL_CHANNELS:
|
||||
return _ALL_CHANNELS
|
||||
|
||||
types = OrderedDict()
|
||||
channels = []
|
||||
for recv, ret in register_sales_channels.send(None):
|
||||
if isinstance(ret, (list, tuple)):
|
||||
for r in ret:
|
||||
types[r.identifier] = r
|
||||
channels += ret
|
||||
else:
|
||||
types[ret.identifier] = ret
|
||||
_ALL_CHANNELS = types
|
||||
return types
|
||||
channels.append(ret)
|
||||
channels.sort(key=lambda c: c.identifier)
|
||||
_ALL_CHANNELS = OrderedDict([(c.identifier, c) for c in channels])
|
||||
if 'web' in _ALL_CHANNELS:
|
||||
_ALL_CHANNELS.move_to_end('web', last=False)
|
||||
return _ALL_CHANNELS
|
||||
|
||||
|
||||
class WebshopSalesChannel(SalesChannel):
|
||||
|
||||
@@ -19,10 +19,7 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import inspect
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal
|
||||
from itertools import groupby
|
||||
from smtplib import SMTPResponseException
|
||||
from typing import TypeVar
|
||||
@@ -33,21 +30,21 @@ from django.core.mail.backends.smtp import EmailBackend
|
||||
from django.db.models import Count
|
||||
from django.dispatch import receiver
|
||||
from django.template.loader import get_template
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import get_language, gettext_lazy as _
|
||||
|
||||
from pretix.base.i18n import (
|
||||
LazyCurrencyNumber, LazyDate, LazyExpiresDate, LazyNumber,
|
||||
)
|
||||
from pretix.base.models import Event
|
||||
from pretix.base.reldate import RelativeDateWrapper
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES, get_name_parts_localized
|
||||
from pretix.base.signals import (
|
||||
register_html_mail_renderers, register_mail_placeholders,
|
||||
)
|
||||
from pretix.base.signals import register_html_mail_renderers
|
||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||
|
||||
from pretix.base.services.placeholders import ( # noqa
|
||||
get_available_placeholders, PlaceholderContext
|
||||
)
|
||||
from pretix.base.services.placeholders import ( # noqa
|
||||
BaseTextPlaceholder as BaseMailTextPlaceholder,
|
||||
SimpleFunctionalTextPlaceholder as SimpleFunctionalMailTextPlaceholder,
|
||||
)
|
||||
from pretix.base.settings import get_name_parts_localized # noqa
|
||||
|
||||
logger = logging.getLogger('pretix.base.email')
|
||||
|
||||
T = TypeVar("T", bound=EmailBackend)
|
||||
@@ -149,6 +146,7 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
||||
}
|
||||
if self.organizer:
|
||||
htmlctx['organizer'] = self.organizer
|
||||
htmlctx['color'] = self.organizer.settings.primary_color
|
||||
|
||||
if self.event:
|
||||
htmlctx['event'] = self.event
|
||||
@@ -191,7 +189,7 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
||||
tpl = get_template(self.template_name)
|
||||
body_html = tpl.render(htmlctx)
|
||||
|
||||
inliner = css_inline.CSSInliner(remove_style_tags=True)
|
||||
inliner = css_inline.CSSInliner(keep_style_tags=False)
|
||||
body_html = inliner.inline(body_html)
|
||||
|
||||
return body_html
|
||||
@@ -216,495 +214,5 @@ def base_renderers(sender, **kwargs):
|
||||
return [ClassicMailRenderer, UnembellishedMailRenderer]
|
||||
|
||||
|
||||
class BaseMailTextPlaceholder:
|
||||
"""
|
||||
This is the base class for for all email text placeholders.
|
||||
"""
|
||||
|
||||
@property
|
||||
def required_context(self):
|
||||
"""
|
||||
This property should return a list of all attribute names that need to be
|
||||
contained in the base context so that this placeholder is available. By default,
|
||||
it returns a list containing the string "event".
|
||||
"""
|
||||
return ["event"]
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
"""
|
||||
This should return the identifier of this placeholder in the email.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def render(self, context):
|
||||
"""
|
||||
This method is called to generate the actual text that is being
|
||||
used in the email. You will be passed a context dictionary with the
|
||||
base context attributes specified in ``required_context``. You are
|
||||
expected to return a plain-text string.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def render_sample(self, event):
|
||||
"""
|
||||
This method is called to generate a text to be used in email previews.
|
||||
This may only depend on the event.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class SimpleFunctionalMailTextPlaceholder(BaseMailTextPlaceholder):
|
||||
def __init__(self, identifier, args, func, sample):
|
||||
self._identifier = identifier
|
||||
self._args = args
|
||||
self._func = func
|
||||
self._sample = sample
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
return self._identifier
|
||||
|
||||
@property
|
||||
def required_context(self):
|
||||
return self._args
|
||||
|
||||
def render(self, context):
|
||||
return self._func(**{k: context[k] for k in self._args})
|
||||
|
||||
def render_sample(self, event):
|
||||
if callable(self._sample):
|
||||
return self._sample(event)
|
||||
else:
|
||||
return self._sample
|
||||
|
||||
|
||||
def get_available_placeholders(event, base_parameters):
|
||||
if 'order' in base_parameters:
|
||||
base_parameters.append('invoice_address')
|
||||
base_parameters.append('position_or_address')
|
||||
params = {}
|
||||
for r, val in register_mail_placeholders.send(sender=event):
|
||||
if not isinstance(val, (list, tuple)):
|
||||
val = [val]
|
||||
for v in val:
|
||||
if all(rp in base_parameters for rp in v.required_context):
|
||||
params[v.identifier] = v
|
||||
return params
|
||||
|
||||
|
||||
def get_email_context(**kwargs):
|
||||
from pretix.base.models import InvoiceAddress
|
||||
|
||||
event = kwargs['event']
|
||||
if 'position' in kwargs:
|
||||
kwargs.setdefault("position_or_address", kwargs['position'])
|
||||
if 'order' in kwargs:
|
||||
try:
|
||||
if not kwargs.get('invoice_address'):
|
||||
kwargs['invoice_address'] = kwargs['order'].invoice_address
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
kwargs['invoice_address'] = InvoiceAddress(order=kwargs['order'])
|
||||
finally:
|
||||
kwargs.setdefault("position_or_address", kwargs['invoice_address'])
|
||||
ctx = {}
|
||||
for r, val in register_mail_placeholders.send(sender=event):
|
||||
if not isinstance(val, (list, tuple)):
|
||||
val = [val]
|
||||
for v in val:
|
||||
if all(rp in kwargs for rp in v.required_context):
|
||||
try:
|
||||
ctx[v.identifier] = v.render(kwargs)
|
||||
except:
|
||||
ctx[v.identifier] = '(error)'
|
||||
logger.exception(f'Failed to process email placeholder {v.identifier}.')
|
||||
return ctx
|
||||
|
||||
|
||||
def _placeholder_payments(order, payments):
|
||||
d = []
|
||||
for payment in payments:
|
||||
if 'payment' in inspect.signature(payment.payment_provider.order_pending_mail_render).parameters:
|
||||
d.append(str(payment.payment_provider.order_pending_mail_render(order, payment)))
|
||||
else:
|
||||
d.append(str(payment.payment_provider.order_pending_mail_render(order)))
|
||||
d = [line for line in d if line.strip()]
|
||||
if d:
|
||||
return '\n\n'.join(d)
|
||||
else:
|
||||
return ''
|
||||
|
||||
|
||||
def get_best_name(position_or_address, parts=False):
|
||||
"""
|
||||
Return the best name we got for either an invoice address or an order position, falling back to the respective other
|
||||
"""
|
||||
from pretix.base.models import InvoiceAddress, OrderPosition
|
||||
if isinstance(position_or_address, InvoiceAddress):
|
||||
if position_or_address.name:
|
||||
return position_or_address.name_parts if parts else position_or_address.name
|
||||
elif position_or_address.order:
|
||||
position_or_address = position_or_address.order.positions.exclude(attendee_name_cached="").exclude(attendee_name_cached__isnull=True).first()
|
||||
|
||||
if isinstance(position_or_address, OrderPosition):
|
||||
if position_or_address.attendee_name:
|
||||
return position_or_address.attendee_name_parts if parts else position_or_address.attendee_name
|
||||
elif position_or_address.order:
|
||||
try:
|
||||
return position_or_address.order.invoice_address.name_parts if parts else position_or_address.order.invoice_address.name
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
pass
|
||||
|
||||
return {} if parts else ""
|
||||
|
||||
|
||||
@receiver(register_mail_placeholders, dispatch_uid="pretixbase_register_mail_placeholders")
|
||||
def base_placeholders(sender, **kwargs):
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
|
||||
ph = [
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'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(
|
||||
'event_slug', ['event'], lambda event: event.slug, lambda event: event.slug
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'code', ['order'], lambda order: order.code, 'F8VVL'
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'total', ['order'], lambda order: LazyNumber(order.total), lambda event: LazyNumber(Decimal('42.23'))
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'currency', ['event'], lambda event: event.currency, lambda event: event.currency
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'order_email', ['order'], lambda order: order.email, 'john@example.org'
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'invoice_number', ['invoice'],
|
||||
lambda invoice: invoice.full_invoice_no,
|
||||
f'{sender.settings.invoice_numbers_prefix or (sender.slug.upper() + "-")}00000'
|
||||
),
|
||||
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(
|
||||
'pending_sum', ['event', 'pending_sum'],
|
||||
lambda event, pending_sum: LazyCurrencyNumber(pending_sum, event.currency),
|
||||
lambda event: LazyCurrencyNumber(Decimal('42.23'), event.currency)
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'total_with_currency', ['event', 'order'], lambda event, order: LazyCurrencyNumber(order.total,
|
||||
event.currency),
|
||||
lambda event: LazyCurrencyNumber(Decimal('42.23'), event.currency)
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'expire_date', ['event', 'order'], lambda event, order: LazyExpiresDate(order.expires.astimezone(event.timezone)),
|
||||
lambda event: LazyDate(now() + timedelta(days=15))
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'url', ['order', 'event'], lambda order, event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.open', kwargs={
|
||||
'order': order.code,
|
||||
'secret': order.secret,
|
||||
'hash': order.email_confirm_hash()
|
||||
}
|
||||
), lambda event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.open', kwargs={
|
||||
'order': 'F8VVL',
|
||||
'secret': '6zzjnumtsx136ddy',
|
||||
'hash': '98kusd8ofsj8dnkd'
|
||||
}
|
||||
),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'url_info_change', ['order', 'event'], lambda order, event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.modify', kwargs={
|
||||
'order': order.code,
|
||||
'secret': order.secret,
|
||||
}
|
||||
), lambda event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.modify', kwargs={
|
||||
'order': 'F8VVL',
|
||||
'secret': '6zzjnumtsx136ddy',
|
||||
}
|
||||
),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'url_products_change', ['order', 'event'], lambda order, event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.change', kwargs={
|
||||
'order': order.code,
|
||||
'secret': order.secret,
|
||||
}
|
||||
), lambda event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.change', kwargs={
|
||||
'order': 'F8VVL',
|
||||
'secret': '6zzjnumtsx136ddy',
|
||||
}
|
||||
),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'url_cancel', ['order', 'event'], lambda order, event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.cancel', kwargs={
|
||||
'order': order.code,
|
||||
'secret': order.secret,
|
||||
}
|
||||
), lambda event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.cancel', kwargs={
|
||||
'order': 'F8VVL',
|
||||
'secret': '6zzjnumtsx136ddy',
|
||||
}
|
||||
),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'url', ['event', 'position'], lambda event, position: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.position',
|
||||
kwargs={
|
||||
'order': position.order.code,
|
||||
'secret': position.web_secret,
|
||||
'position': position.positionid
|
||||
}
|
||||
),
|
||||
lambda event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.position', kwargs={
|
||||
'order': 'F8VVL',
|
||||
'secret': '6zzjnumtsx136ddy',
|
||||
'position': '123'
|
||||
}
|
||||
),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'order_modification_deadline_date_and_time', ['order', 'event'],
|
||||
lambda order, event:
|
||||
date_format(order.modify_deadline.astimezone(event.timezone), 'SHORT_DATETIME_FORMAT')
|
||||
if order.modify_deadline
|
||||
else '',
|
||||
lambda event: date_format(
|
||||
event.settings.get(
|
||||
'last_order_modification_date', as_type=RelativeDateWrapper
|
||||
).datetime(event).astimezone(event.timezone),
|
||||
'SHORT_DATETIME_FORMAT'
|
||||
) if event.settings.get('last_order_modification_date') else '',
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'event_location', ['event_or_subevent'], lambda event_or_subevent: str(event_or_subevent.location or ''),
|
||||
lambda event: str(event.location or ''),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'event_admission_time', ['event_or_subevent'],
|
||||
lambda event_or_subevent:
|
||||
date_format(event_or_subevent.date_admission.astimezone(event_or_subevent.timezone), 'TIME_FORMAT')
|
||||
if event_or_subevent.date_admission
|
||||
else '',
|
||||
lambda event: date_format(event.date_admission.astimezone(event.timezone), 'TIME_FORMAT') if event.date_admission else '',
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'subevent', ['waiting_list_entry', 'event'],
|
||||
lambda waiting_list_entry, event: str(waiting_list_entry.subevent or event),
|
||||
lambda event: str(event if not event.has_subevents or not event.subevents.exists() else event.subevents.first())
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'subevent_date_from', ['waiting_list_entry', 'event'],
|
||||
lambda waiting_list_entry, event: (waiting_list_entry.subevent or event).get_date_from_display(),
|
||||
lambda event: (event if not event.has_subevents or not event.subevents.exists() else event.subevents.first()).get_date_from_display()
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'url_remove', ['waiting_list_voucher', 'event'],
|
||||
lambda waiting_list_voucher, event: build_absolute_uri(
|
||||
event, 'presale:event.waitinglist.remove'
|
||||
) + '?voucher=' + waiting_list_voucher.code,
|
||||
lambda event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.waitinglist.remove',
|
||||
) + '?voucher=68CYU2H6ZTP3WLK5',
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'url', ['waiting_list_voucher', 'event'],
|
||||
lambda waiting_list_voucher, event: build_absolute_uri(
|
||||
event, 'presale:event.redeem'
|
||||
) + '?voucher=' + waiting_list_voucher.code,
|
||||
lambda event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.redeem',
|
||||
) + '?voucher=68CYU2H6ZTP3WLK5',
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'invoice_name', ['invoice_address'], lambda invoice_address: invoice_address.name or '',
|
||||
_('John Doe')
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'invoice_company', ['invoice_address'], lambda invoice_address: invoice_address.company or '',
|
||||
_('Sample Corporation')
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'orders', ['event', 'orders'], lambda event, orders: '\n' + '\n\n'.join(
|
||||
'* {} - {}'.format(
|
||||
order.full_code,
|
||||
build_absolute_uri(event, 'presale:event.order.open', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
'order': order.code,
|
||||
'secret': order.secret,
|
||||
'hash': order.email_confirm_hash(),
|
||||
}),
|
||||
)
|
||||
for order in orders
|
||||
), lambda event: '\n' + '\n\n'.join(
|
||||
'* {} - {}'.format(
|
||||
'{}-{}'.format(event.slug.upper(), order['code']),
|
||||
build_absolute_uri(event, 'presale:event.order.open', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
'order': order['code'],
|
||||
'secret': order['secret'],
|
||||
'hash': order['hash'],
|
||||
}),
|
||||
)
|
||||
for order in [
|
||||
{'code': 'F8VVL', 'secret': '6zzjnumtsx136ddy', 'hash': 'abcdefghi'},
|
||||
{'code': 'HIDHK', 'secret': '98kusd8ofsj8dnkd', 'hash': 'jklmnopqr'},
|
||||
{'code': 'OPKSB', 'secret': '09pjdksflosk3njd', 'hash': 'stuvwxy2z'}
|
||||
]
|
||||
),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'hours', ['event', 'waiting_list_entry'], lambda event, waiting_list_entry:
|
||||
event.settings.waiting_list_hours,
|
||||
lambda event: event.settings.waiting_list_hours
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'product', ['waiting_list_entry'], lambda waiting_list_entry: waiting_list_entry.item.name,
|
||||
_('Sample Admission Ticket')
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'code', ['waiting_list_voucher'], lambda waiting_list_voucher: waiting_list_voucher.code,
|
||||
'68CYU2H6ZTP3WLK5'
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
|
||||
'voucher_list', ['voucher_list'], lambda voucher_list: ' \n'.join(voucher_list),
|
||||
' 68CYU2H6ZTP3WLK5\n 7MB94KKPVEPSMVF2'
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
|
||||
'voucher_url_list', ['event', 'voucher_list'],
|
||||
lambda event, voucher_list: ' \n'.join([
|
||||
build_absolute_uri(
|
||||
event, 'presale:event.redeem'
|
||||
) + '?voucher=' + c
|
||||
for c in voucher_list
|
||||
]),
|
||||
lambda event: ' \n'.join([
|
||||
build_absolute_uri(
|
||||
event, 'presale:event.redeem'
|
||||
) + '?voucher=' + c
|
||||
for c in ['68CYU2H6ZTP3WLK5', '7MB94KKPVEPSMVF2']
|
||||
]),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'url', ['event', 'voucher_list'], lambda event, voucher_list: build_absolute_uri(event, 'presale:event.index', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
}), lambda event: build_absolute_uri(event, 'presale:event.index', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
})
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'name', ['name'], lambda name: name,
|
||||
_('John Doe')
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'comment', ['comment'], lambda comment: comment,
|
||||
_('An individual text with a reason can be inserted here.'),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'payment_info', ['order', 'payments'], _placeholder_payments,
|
||||
_('The amount has been charged to your card.'),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'payment_info', ['payment_info'], lambda payment_info: payment_info,
|
||||
_('Please transfer money to this bank account: 9999-9999-9999-9999'),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'attendee_name', ['position'], lambda position: position.attendee_name,
|
||||
_('John Doe'),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'positionid', ['position'], lambda position: str(position.positionid),
|
||||
'1'
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'name', ['position_or_address'],
|
||||
get_best_name,
|
||||
_('John Doe'),
|
||||
),
|
||||
]
|
||||
|
||||
name_scheme = PERSON_NAME_SCHEMES[sender.settings.name_scheme]
|
||||
if "concatenation_for_salutation" in name_scheme:
|
||||
concatenation_for_salutation = name_scheme["concatenation_for_salutation"]
|
||||
else:
|
||||
concatenation_for_salutation = name_scheme["concatenation"]
|
||||
|
||||
ph.append(SimpleFunctionalMailTextPlaceholder(
|
||||
"name_for_salutation", ["waiting_list_entry"],
|
||||
lambda waiting_list_entry: concatenation_for_salutation(waiting_list_entry.name_parts),
|
||||
_("Mr Doe"),
|
||||
))
|
||||
ph.append(SimpleFunctionalMailTextPlaceholder(
|
||||
"name", ["waiting_list_entry"],
|
||||
lambda waiting_list_entry: waiting_list_entry.name or "",
|
||||
_("Mr Doe"),
|
||||
))
|
||||
ph.append(SimpleFunctionalMailTextPlaceholder(
|
||||
"name_for_salutation", ["position_or_address"],
|
||||
lambda position_or_address: concatenation_for_salutation(get_best_name(position_or_address, parts=True)),
|
||||
_("Mr Doe"),
|
||||
))
|
||||
|
||||
for f, l, w in name_scheme['fields']:
|
||||
if f == 'full_name':
|
||||
continue
|
||||
ph.append(SimpleFunctionalMailTextPlaceholder(
|
||||
'name_%s' % f, ['waiting_list_entry'], lambda waiting_list_entry, f=f: get_name_parts_localized(waiting_list_entry.name_parts, f),
|
||||
name_scheme['sample'][f]
|
||||
))
|
||||
ph.append(SimpleFunctionalMailTextPlaceholder(
|
||||
'attendee_name_%s' % f, ['position'], lambda position, f=f: get_name_parts_localized(position.attendee_name_parts, f),
|
||||
name_scheme['sample'][f]
|
||||
))
|
||||
ph.append(SimpleFunctionalMailTextPlaceholder(
|
||||
'name_%s' % f, ['position_or_address'],
|
||||
lambda position_or_address, f=f: get_name_parts_localized(get_best_name(position_or_address, parts=True), f),
|
||||
name_scheme['sample'][f]
|
||||
))
|
||||
|
||||
for k, v in sender.meta_data.items():
|
||||
ph.append(SimpleFunctionalMailTextPlaceholder(
|
||||
'meta_%s' % k, ['event'], lambda event, k=k: event.meta_data[k],
|
||||
v
|
||||
))
|
||||
ph.append(SimpleFunctionalMailTextPlaceholder(
|
||||
'meta_%s' % k, ['event_or_subevent'], lambda event_or_subevent, k=k: event_or_subevent.meta_data[k],
|
||||
v
|
||||
))
|
||||
|
||||
return ph
|
||||
return PlaceholderContext(**kwargs).render_all()
|
||||
|
||||
@@ -28,4 +28,5 @@ from .items import * # noqa
|
||||
from .json import * # noqa
|
||||
from .mail import * # noqa
|
||||
from .orderlist import * # noqa
|
||||
from .reusablemedia import * # noqa
|
||||
from .waitinglist import * # noqa
|
||||
|
||||
@@ -39,10 +39,12 @@ from zipfile import ZipFile
|
||||
|
||||
from django import forms
|
||||
from django.dispatch import receiver
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
|
||||
from pretix.base.models import QuestionAnswer
|
||||
|
||||
from ...control.forms.widgets import Select2
|
||||
from ..exporter import BaseExporter
|
||||
from ..signals import register_data_exporters
|
||||
|
||||
@@ -56,7 +58,7 @@ class AnswerFilesExporter(BaseExporter):
|
||||
|
||||
@property
|
||||
def export_form_fields(self):
|
||||
return OrderedDict(
|
||||
d = OrderedDict(
|
||||
[
|
||||
('questions',
|
||||
forms.ModelMultipleChoiceField(
|
||||
@@ -69,11 +71,32 @@ class AnswerFilesExporter(BaseExporter):
|
||||
)),
|
||||
]
|
||||
)
|
||||
if self.event.has_subevents:
|
||||
d['subevent'] = forms.ModelChoiceField(
|
||||
label=pgettext_lazy('subevent', 'Date'),
|
||||
queryset=self.event.subevents.all(),
|
||||
required=False,
|
||||
empty_label=pgettext_lazy('subevent', 'All dates')
|
||||
)
|
||||
d['subevent'].widget = Select2(
|
||||
attrs={
|
||||
'data-model-select2': 'event',
|
||||
'data-select2-url': reverse('control:event.subevents.select2', kwargs={
|
||||
'event': self.event.slug,
|
||||
'organizer': self.event.organizer.slug,
|
||||
}),
|
||||
'data-placeholder': pgettext_lazy('subevent', 'All dates')
|
||||
}
|
||||
)
|
||||
d['subevent'].widget.choices = d['subevent'].choices
|
||||
return d
|
||||
|
||||
def render(self, form_data: dict):
|
||||
qs = QuestionAnswer.objects.filter(
|
||||
orderposition__order__event=self.event,
|
||||
).select_related('orderposition', 'orderposition__order', 'question')
|
||||
if form_data.get('subevent'):
|
||||
qs = qs.filter(orderposition__subevent=form_data.get('subevent'))
|
||||
if form_data.get('questions'):
|
||||
qs = qs.filter(question__in=form_data['questions'])
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
|
||||
@@ -116,15 +116,29 @@ class DekodiNREIExporter(BaseExporter):
|
||||
'PTNo15': p.full_id or '',
|
||||
})
|
||||
elif p.provider and p.provider.startswith('stripe'):
|
||||
src = p.info_data.get("source", p.info_data)
|
||||
pi = p.info_data or {}
|
||||
try:
|
||||
if "latest_charge" in pi and isinstance(pi.get("latest_charge"), dict):
|
||||
details = pi["latest_charge"]["payment_method_details"]
|
||||
card = details.get("card", {})
|
||||
elif pi.get("charges") and pi["charges"]["data"]:
|
||||
details = pi["charges"]["data"][0].get("payment_method_details", {})
|
||||
card = details.get("card", {})
|
||||
else:
|
||||
details = pi["source"]
|
||||
card = pi["source"]["card"]
|
||||
except:
|
||||
details = {}
|
||||
card = {}
|
||||
|
||||
payments.append({
|
||||
'PTID': '81',
|
||||
'PTN': 'Stripe',
|
||||
'PTNo1': p.info_data.get("id") or '',
|
||||
'PTNo5': src.get("card", {}).get("last4") or '',
|
||||
'PTNo1': pi.get("id") or '',
|
||||
'PTNo5': card.get("last4", ""),
|
||||
'PTNo7': round(float(p.amount), 2) or '',
|
||||
'PTNo8': str(self.event.currency) or '',
|
||||
'PTNo10': src.get('owner', {}).get('verified_name') or src.get('owner', {}).get('name') or '',
|
||||
'PTNo10': details.get('owner', {}).get('verified_name') or details.get('owner', {}).get('name') or '',
|
||||
'PTNo15': p.full_id or '',
|
||||
})
|
||||
else:
|
||||
|
||||
@@ -86,6 +86,7 @@ class InvoiceExporterMixin:
|
||||
('', _('All payment providers')),
|
||||
] + [
|
||||
(k, v.verbose_name) for k, v in self.event.get_payment_providers().items()
|
||||
if not v.is_meta
|
||||
],
|
||||
required=False,
|
||||
help_text=_('Only include invoices for orders that have at least one payment attempt '
|
||||
|
||||
@@ -88,6 +88,7 @@ class ItemDataExporter(ListExporter):
|
||||
_("Minimum amount per order"),
|
||||
_("Maximum amount per order"),
|
||||
_("Requires special attention"),
|
||||
_("Check-in text"),
|
||||
_("Original price"),
|
||||
_("This product is a gift card"),
|
||||
_("Require a valid membership"),
|
||||
@@ -162,6 +163,7 @@ class ItemDataExporter(ListExporter):
|
||||
i.min_per_order if i.min_per_order is not None else "",
|
||||
i.max_per_order if i.max_per_order is not None else "",
|
||||
_("Yes") if i.checkin_attention else "",
|
||||
i.checkin_text or "",
|
||||
v.original_price or i.original_price or "",
|
||||
_("Yes") if i.issue_giftcard else "",
|
||||
_("Yes") if i.require_membership or v.require_membership else "",
|
||||
@@ -206,6 +208,7 @@ class ItemDataExporter(ListExporter):
|
||||
i.min_per_order if i.min_per_order is not None else "",
|
||||
i.max_per_order if i.max_per_order is not None else "",
|
||||
_("Yes") if i.checkin_attention else "",
|
||||
i.checkin_text or "",
|
||||
i.original_price or "",
|
||||
_("Yes") if i.issue_giftcard else "",
|
||||
_("Yes") if i.require_membership else "",
|
||||
|
||||
@@ -96,6 +96,7 @@ class JSONExporter(BaseExporter):
|
||||
'min_per_order': item.min_per_order,
|
||||
'max_per_order': item.max_per_order,
|
||||
'checkin_attention': item.checkin_attention,
|
||||
'checkin_text': item.checkin_text,
|
||||
'original_price': item.original_price,
|
||||
'issue_giftcard': item.issue_giftcard,
|
||||
'meta_data': item.meta_data,
|
||||
@@ -110,6 +111,7 @@ class JSONExporter(BaseExporter):
|
||||
'description': str(variation.description),
|
||||
'position': variation.position,
|
||||
'checkin_attention': variation.checkin_attention,
|
||||
'checkin_text': variation.checkin_text,
|
||||
'require_approval': variation.require_approval,
|
||||
'require_membership': variation.require_membership,
|
||||
'sales_channels': variation.sales_channels,
|
||||
@@ -164,6 +166,7 @@ class JSONExporter(BaseExporter):
|
||||
'custom_followup_at': order.custom_followup_at,
|
||||
'require_approval': order.require_approval,
|
||||
'checkin_attention': order.checkin_attention,
|
||||
'checkin_text': order.checkin_text,
|
||||
'sales_channel': order.sales_channel,
|
||||
'expires': order.expires,
|
||||
'datetime': order.datetime,
|
||||
|
||||
@@ -209,7 +209,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
return qs.annotate(**annotations).filter(**filters)
|
||||
return qs
|
||||
|
||||
def iterate_orders(self, form_data: dict):
|
||||
def orders_qs(self, form_data):
|
||||
p_date = OrderPayment.objects.filter(
|
||||
order=OuterRef('pk'),
|
||||
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED),
|
||||
@@ -250,11 +250,15 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
|
||||
if form_data['paid_only']:
|
||||
qs = qs.filter(status=Order.STATUS_PAID)
|
||||
return qs
|
||||
|
||||
def iterate_orders(self, form_data: dict):
|
||||
qs = self.orders_qs(form_data)
|
||||
tax_rates = self._get_all_tax_rates(qs)
|
||||
|
||||
headers = [
|
||||
_('Event slug'), _('Order code'), _('Order total'), _('Status'), _('Email'), _('Phone number'),
|
||||
_('Order date'), _('Order time'), _('Company'), _('Name'),
|
||||
_('Event slug'), _('Event name'), _('Order code'), _('Order total'), _('Status'), _('Email'),
|
||||
_('Phone number'), _('Order date'), _('Order time'), _('Company'), _('Name'),
|
||||
]
|
||||
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme] if not self.is_multievent else None
|
||||
if name_scheme and len(name_scheme['fields']) > 1:
|
||||
@@ -275,6 +279,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
headers.append(_('Invoice numbers'))
|
||||
headers.append(_('Sales channel'))
|
||||
headers.append(_('Requires special attention'))
|
||||
headers.append(_('Check-in text'))
|
||||
headers.append(_('Comment'))
|
||||
headers.append(_('Follow-up date'))
|
||||
headers.append(_('Positions'))
|
||||
@@ -330,9 +335,10 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
|
||||
row = [
|
||||
self.event_object_cache[order.event_id].slug,
|
||||
str(self.event_object_cache[order.event_id].name),
|
||||
order.code,
|
||||
order.total,
|
||||
order.get_status_display(),
|
||||
order.get_extended_status_display(),
|
||||
order.email,
|
||||
str(order.phone) if order.phone else '',
|
||||
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
|
||||
@@ -384,6 +390,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
row.append(order.invoice_numbers)
|
||||
row.append(order.sales_channel)
|
||||
row.append(_('Yes') if order.checkin_attention else _('No'))
|
||||
row.append(order.checkin_text or "")
|
||||
row.append(order.comment or "")
|
||||
row.append(order.custom_followup_at.strftime("%Y-%m-%d") if order.custom_followup_at else "")
|
||||
row.append(order.pcnt)
|
||||
@@ -404,7 +411,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
row += self.event_object_cache[order.event_id].meta_data.values()
|
||||
yield row
|
||||
|
||||
def iterate_fees(self, form_data: dict):
|
||||
def fees_qs(self, form_data):
|
||||
p_providers = OrderPayment.objects.filter(
|
||||
order=OuterRef('order'),
|
||||
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED,
|
||||
@@ -423,9 +430,14 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
qs = qs.filter(order__status=Order.STATUS_PAID, canceled=False)
|
||||
|
||||
qs = self._date_filter(qs, form_data, rel='order__')
|
||||
return qs
|
||||
|
||||
def iterate_fees(self, form_data: dict):
|
||||
qs = self.fees_qs(form_data)
|
||||
|
||||
headers = [
|
||||
_('Event slug'),
|
||||
_('Event name'),
|
||||
_('Order code'),
|
||||
_('Status'),
|
||||
_('Email'),
|
||||
@@ -462,8 +474,9 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
tz = ZoneInfo(order.event.settings.timezone)
|
||||
row = [
|
||||
self.event_object_cache[order.event_id].slug,
|
||||
str(self.event_object_cache[order.event_id].name),
|
||||
order.code,
|
||||
_("canceled") if op.canceled else order.get_status_display(),
|
||||
_("canceled") if op.canceled else order.get_extended_status_display(),
|
||||
order.email,
|
||||
str(order.phone) if order.phone else '',
|
||||
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
|
||||
@@ -504,7 +517,19 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
row += self.event_object_cache[order.event_id].meta_data.values()
|
||||
yield row
|
||||
|
||||
def positions_qs(self, form_data: dict):
|
||||
qs = OrderPosition.all.filter(
|
||||
order__event__in=self.events,
|
||||
)
|
||||
if form_data['paid_only']:
|
||||
qs = qs.filter(order__status=Order.STATUS_PAID, canceled=False)
|
||||
|
||||
qs = self._date_filter(qs, form_data, rel='order__')
|
||||
return qs
|
||||
|
||||
def iterate_positions(self, form_data: dict):
|
||||
base_qs = self.positions_qs(form_data)
|
||||
|
||||
p_providers = OrderPayment.objects.filter(
|
||||
order=OuterRef('order'),
|
||||
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED,
|
||||
@@ -514,9 +539,6 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
).values(
|
||||
'm'
|
||||
).order_by()
|
||||
base_qs = OrderPosition.all.filter(
|
||||
order__event__in=self.events,
|
||||
)
|
||||
qs = base_qs.annotate(
|
||||
payment_providers=Subquery(p_providers, output_field=CharField()),
|
||||
).select_related(
|
||||
@@ -526,15 +548,12 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
'subevent', 'subevent__meta_values',
|
||||
'answers', 'answers__question', 'answers__options'
|
||||
)
|
||||
if form_data['paid_only']:
|
||||
qs = qs.filter(order__status=Order.STATUS_PAID, canceled=False)
|
||||
|
||||
qs = self._date_filter(qs, form_data, rel='order__')
|
||||
|
||||
has_subevents = self.events.filter(has_subevents=True).exists()
|
||||
|
||||
headers = [
|
||||
_('Event slug'),
|
||||
_('Event name'),
|
||||
_('Order code'),
|
||||
_('Position ID'),
|
||||
_('Status'),
|
||||
@@ -636,9 +655,10 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
tz = ZoneInfo(self.event_object_cache[order.event_id].settings.timezone)
|
||||
row = [
|
||||
self.event_object_cache[order.event_id].slug,
|
||||
str(self.event_object_cache[order.event_id].name),
|
||||
order.code,
|
||||
op.positionid,
|
||||
_("canceled") if op.canceled else order.get_status_display(),
|
||||
_("canceled") if op.canceled else order.get_extended_status_display(),
|
||||
order.email,
|
||||
str(order.phone) if order.phone else '',
|
||||
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
|
||||
@@ -1007,20 +1027,20 @@ class PaymentListExporter(ListExporter):
|
||||
if form_data.get('end_date_range'):
|
||||
dt_start, dt_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['end_date_range'], self.timezone)
|
||||
if dt_start:
|
||||
payments = payments.filter(created__gte=dt_start)
|
||||
refunds = refunds .filter(created__gte=dt_start)
|
||||
payments = payments.filter(payment_date__gte=dt_start)
|
||||
refunds = refunds.filter(execution_date__gte=dt_start)
|
||||
if dt_end:
|
||||
payments = payments.filter(created__lt=dt_end)
|
||||
refunds = refunds .filter(created__lt=dt_end)
|
||||
payments = payments.filter(payment_date__lt=dt_end)
|
||||
refunds = refunds.filter(execution_date__lt=dt_end)
|
||||
|
||||
if form_data.get('start_end_date_range'):
|
||||
dt_start, dt_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['start_date_range'], self.timezone)
|
||||
if dt_start:
|
||||
payments = payments.filter(payment_date__gte=dt_start)
|
||||
refunds = refunds .filter(execution_date__gte=dt_start)
|
||||
payments = payments.filter(created__gte=dt_start)
|
||||
refunds = refunds.filter(created__gte=dt_start)
|
||||
if dt_end:
|
||||
payments = payments.filter(payment_date__lt=dt_end)
|
||||
refunds = refunds.filter(execution_date__lt=dt_end)
|
||||
payments = payments.filter(created__lt=dt_end)
|
||||
refunds = refunds.filter(created__lt=dt_end)
|
||||
|
||||
objs = sorted(list(payments) + list(refunds), key=lambda o: o.created)
|
||||
|
||||
|
||||
78
src/pretix/base/exporters/reusablemedia.py
Normal file
78
src/pretix/base/exporters/reusablemedia.py
Normal file
@@ -0,0 +1,78 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.translation import gettext_lazy as _, pgettext, pgettext_lazy
|
||||
|
||||
from ..exporter import ListExporter, OrganizerLevelExportMixin
|
||||
from ..models import ReusableMedium
|
||||
from ..signals import register_multievent_data_exporters
|
||||
|
||||
|
||||
class ReusableMediaExporter(OrganizerLevelExportMixin, ListExporter):
|
||||
identifier = 'reusablemedia'
|
||||
verbose_name = _('Reusable media')
|
||||
category = pgettext_lazy('export_category', 'Reusable media')
|
||||
description = _('Download a spread sheet with the data of all reusable medias on your account.')
|
||||
|
||||
def iterate_list(self, form_data):
|
||||
media = ReusableMedium.objects.filter(
|
||||
organizer=self.organizer,
|
||||
).select_related(
|
||||
'customer', 'linked_orderposition', 'linked_giftcard',
|
||||
).order_by('created')
|
||||
|
||||
headers = [
|
||||
pgettext('reusable_medium', 'Media type'),
|
||||
pgettext('reusable_medium', 'Identifier'),
|
||||
_('Active'),
|
||||
_('Expiration date'),
|
||||
_('Customer account'),
|
||||
_('Linked ticket'),
|
||||
_('Linked gift card'),
|
||||
_('Notes'),
|
||||
]
|
||||
|
||||
yield headers
|
||||
yield self.ProgressSetTotal(total=media.count())
|
||||
|
||||
for medium in media.iterator(chunk_size=1000):
|
||||
row = [
|
||||
medium.type,
|
||||
medium.identifier,
|
||||
_('Yes') if medium.active else _('No'),
|
||||
date_format(medium.expires, 'SHORT_DATETIME_FORMAT') if medium.expires else '',
|
||||
medium.customer.identifier if medium.customer_id else '',
|
||||
f"{medium.linked_orderposition.order.code}-{medium.linked_orderposition.positionid}" if medium.linked_orderposition_id else '',
|
||||
medium.linked_giftcard.secret if medium.linked_giftcard_id else '',
|
||||
medium.notes,
|
||||
]
|
||||
yield row
|
||||
|
||||
def get_filename(self):
|
||||
return f'{self.organizer.slug}_media'
|
||||
|
||||
|
||||
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_reusablemedia")
|
||||
def register_multievent_i_reusable_media_exporter(sender, **kwargs):
|
||||
return ReusableMediaExporter
|
||||
@@ -39,6 +39,7 @@ from django import forms
|
||||
from django.core.validators import URLValidator
|
||||
from django.forms.models import ModelFormMetaclass
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from formtools.wizard.views import SessionWizardView
|
||||
from hierarkey.forms import HierarkeyForm
|
||||
@@ -85,6 +86,43 @@ class I18nInlineFormSet(i18nfield.forms.I18nInlineFormSet):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class MarkdownTextarea(forms.Textarea):
|
||||
|
||||
def _render(self, template_name, context, renderer=None):
|
||||
return mark_safe(
|
||||
'<div class="i18n-form-group">%s<div class="i18n-field-markdown-note">%s</div></div>' % (
|
||||
super()._render(template_name, context, renderer=None),
|
||||
_("You can use {markup_name} in this field.").format(
|
||||
markup_name='<a href="https://docs.pretix.eu/en/latest/user/markdown.html" target="_blank">Markdown</a>'
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class I18nMarkdownTextarea(i18nfield.forms.I18nTextarea):
|
||||
def format_output(self, rendered_widgets) -> str:
|
||||
rendered_widgets = rendered_widgets + [
|
||||
'<div class="i18n-field-markdown-note">%s</div>' % (
|
||||
_("You can use {markup_name} in this field.").format(
|
||||
markup_name='<a href="https://docs.pretix.eu/en/latest/user/markdown.html" target="_blank">Markdown</a>'
|
||||
)
|
||||
)
|
||||
]
|
||||
return super().format_output(rendered_widgets)
|
||||
|
||||
|
||||
class I18nMarkdownTextInput(i18nfield.forms.I18nTextInput):
|
||||
def format_output(self, rendered_widgets) -> str:
|
||||
rendered_widgets = rendered_widgets + [
|
||||
'<div class="i18n-field-markdown-note">%s</div>' % (
|
||||
_("You can use {markup_name} in this field.").format(
|
||||
markup_name='<a href="https://docs.pretix.eu/en/latest/user/markdown.html" target="_blank">Markdown</a>'
|
||||
)
|
||||
)
|
||||
]
|
||||
return super().format_output(rendered_widgets)
|
||||
|
||||
|
||||
SECRET_REDACTED = '*****'
|
||||
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
|
||||
import hashlib
|
||||
import ipaddress
|
||||
import logging
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
@@ -44,10 +45,13 @@ from django.contrib.auth.password_validation import (
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.metrics import pretix_failed_logins
|
||||
from pretix.base.models import User
|
||||
from pretix.helpers.dicts import move_to_end
|
||||
from pretix.helpers.http import get_client_ip
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LoginForm(forms.Form):
|
||||
"""
|
||||
@@ -55,6 +59,7 @@ class LoginForm(forms.Form):
|
||||
username/password logins.
|
||||
"""
|
||||
keep_logged_in = forms.BooleanField(label=_("Keep me logged in"), required=False)
|
||||
origin = forms.CharField(widget=forms.HiddenInput, required=False)
|
||||
|
||||
error_messages = {
|
||||
'invalid_login': _("This combination of credentials is not known to our system."),
|
||||
@@ -104,12 +109,16 @@ class LoginForm(forms.Form):
|
||||
rc = get_redis_connection("redis")
|
||||
cnt = rc.get(self.ratelimit_key)
|
||||
if cnt and int(cnt) > 10:
|
||||
pretix_failed_logins.inc(1, reason="ratelimit")
|
||||
logger.info("Backend login rejected due to rate limit.")
|
||||
raise forms.ValidationError(self.error_messages['rate_limit'], code='rate_limit')
|
||||
self.user_cache = self.backend.form_authenticate(self.request, self.cleaned_data)
|
||||
if self.user_cache is None:
|
||||
if self.ratelimit_key:
|
||||
rc.incr(self.ratelimit_key)
|
||||
rc.expire(self.ratelimit_key, 300)
|
||||
logger.info("Backend login invalid.")
|
||||
pretix_failed_logins.inc(1, reason="invalid")
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['invalid_login'],
|
||||
code='invalid_login'
|
||||
@@ -131,6 +140,8 @@ class LoginForm(forms.Form):
|
||||
If the given user may log in, this method should return None.
|
||||
"""
|
||||
if not user.is_active:
|
||||
logger.info("Backend login rejected due to user inactive.")
|
||||
pretix_failed_logins.inc(1, reason="inactive")
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['inactive'],
|
||||
code='inactive',
|
||||
|
||||
@@ -57,7 +57,7 @@ from django.forms.widgets import FILE_INPUT_CONTRADICTION
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.timezone import get_current_timezone, now
|
||||
from django.utils.timezone import get_current_timezone
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_countries import countries
|
||||
from django_countries.fields import Country, CountryField
|
||||
@@ -86,6 +86,7 @@ from pretix.base.settings import (
|
||||
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS,
|
||||
)
|
||||
from pretix.base.templatetags.rich_text import rich_text
|
||||
from pretix.base.timemachine import time_machine_now
|
||||
from pretix.control.forms import (
|
||||
ExtFileField, ExtValidationMixin, SizeValidationMixin, SplitDateTimeField,
|
||||
)
|
||||
@@ -125,7 +126,7 @@ class NamePartsWidget(forms.MultiWidget):
|
||||
if fname == 'title' and self.titles:
|
||||
widgets.append(Select(attrs=a, choices=[('', '')] + [(d, d) for d in self.titles[1]]))
|
||||
elif fname == 'salutation':
|
||||
widgets.append(Select(attrs=a, choices=[('', '---')] + PERSON_NAME_SALUTATIONS))
|
||||
widgets.append(Select(attrs=a, choices=[('', '---'), ('empty', '')] + PERSON_NAME_SALUTATIONS))
|
||||
else:
|
||||
widgets.append(self.widget(attrs=a))
|
||||
super().__init__(widgets, attrs)
|
||||
@@ -136,7 +137,10 @@ class NamePartsWidget(forms.MultiWidget):
|
||||
data = []
|
||||
for i, field in enumerate(self.scheme['fields']):
|
||||
fname, label, size = field
|
||||
data.append(value.get(fname, ""))
|
||||
fval = value.get(fname, "")
|
||||
if fname == "salutation" and fname in value and fval == "":
|
||||
fval = "empty"
|
||||
data.append(fval)
|
||||
if '_legacy' in value and not data[-1]:
|
||||
data[-1] = value.get('_legacy', '')
|
||||
elif not any(d for d in data) and '_scheme' in value:
|
||||
@@ -190,7 +194,8 @@ class NamePartsFormField(forms.MultiValueField):
|
||||
data = {}
|
||||
data['_scheme'] = self.scheme_name
|
||||
for i, value in enumerate(data_list):
|
||||
data[self.scheme['fields'][i][0]] = value or ''
|
||||
key = self.scheme['fields'][i][0]
|
||||
data[key] = value or ''
|
||||
return data
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -239,7 +244,7 @@ class NamePartsFormField(forms.MultiValueField):
|
||||
d.pop('validators', None)
|
||||
field = forms.ChoiceField(
|
||||
**d,
|
||||
choices=[('', '---')] + PERSON_NAME_SALUTATIONS
|
||||
choices=[('', '---'), ('empty', '')] + PERSON_NAME_SALUTATIONS
|
||||
)
|
||||
else:
|
||||
field = forms.CharField(**defaults)
|
||||
@@ -265,6 +270,9 @@ class NamePartsFormField(forms.MultiValueField):
|
||||
if sum(len(v) for v in value.values() if v) > 250:
|
||||
raise forms.ValidationError(_('Please enter a shorter name.'), code='max_length')
|
||||
|
||||
if value.get("salutation") == "empty":
|
||||
value["salutation"] = ""
|
||||
|
||||
return value
|
||||
|
||||
|
||||
@@ -599,30 +607,41 @@ class BaseQuestionsForm(forms.Form):
|
||||
|
||||
if cartpos and item.validity_mode == Item.VALIDITY_MODE_DYNAMIC and item.validity_dynamic_start_choice:
|
||||
if item.validity_dynamic_start_choice_day_limit:
|
||||
max_date = now().astimezone(event.timezone) + timedelta(days=item.validity_dynamic_start_choice_day_limit)
|
||||
max_date = time_machine_now().astimezone(event.timezone) + timedelta(days=item.validity_dynamic_start_choice_day_limit)
|
||||
else:
|
||||
max_date = None
|
||||
min_date = time_machine_now()
|
||||
initial = None
|
||||
if (item.require_membership or (pos.variation and pos.variation.require_membership)) and pos.used_membership:
|
||||
if pos.used_membership.date_start >= time_machine_now():
|
||||
initial = min_date = pos.used_membership.date_start
|
||||
max_date = min(max_date, pos.used_membership.date_end) if max_date else pos.used_membership.date_end
|
||||
if item.validity_dynamic_duration_months or item.validity_dynamic_duration_days:
|
||||
attrs = {}
|
||||
if max_date:
|
||||
attrs['data-max'] = max_date.date().isoformat()
|
||||
if min_date:
|
||||
attrs['data-min'] = min_date.date().isoformat()
|
||||
self.fields['requested_valid_from'] = forms.DateField(
|
||||
label=_('Start date'),
|
||||
help_text=_('If you keep this empty, the ticket will be valid starting at the time of purchase.'),
|
||||
required=False,
|
||||
help_text='' if initial else _('If you keep this empty, the ticket will be valid starting at the time of purchase.'),
|
||||
required=bool(initial),
|
||||
initial=pos.requested_valid_from or initial,
|
||||
widget=DatePickerWidget(attrs),
|
||||
validators=[MaxDateValidator(max_date.date())] if max_date else []
|
||||
validators=([MaxDateValidator(max_date.date())] if max_date else []) + [MinDateValidator(min_date.date())]
|
||||
)
|
||||
else:
|
||||
self.fields['requested_valid_from'] = forms.SplitDateTimeField(
|
||||
label=_('Start date'),
|
||||
help_text=_('If you keep this empty, the ticket will be valid starting at the time of purchase.'),
|
||||
required=False,
|
||||
help_text='' if initial else _('If you keep this empty, the ticket will be valid starting at the time of purchase.'),
|
||||
required=bool(initial),
|
||||
initial=pos.requested_valid_from or initial,
|
||||
widget=SplitDateTimePickerWidget(
|
||||
time_format=get_format_without_seconds('TIME_INPUT_FORMATS'),
|
||||
min_date=min_date,
|
||||
max_date=max_date
|
||||
),
|
||||
validators=[MaxDateTimeValidator(max_date)] if max_date else []
|
||||
validators=([MaxDateTimeValidator(max_date)] if max_date else []) + [MinDateTimeValidator(min_date)]
|
||||
)
|
||||
|
||||
add_fields = {}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from bootstrap3.renderers import (
|
||||
FieldRenderer as BaseFieldRenderer,
|
||||
InlineFieldRenderer as BaseInlineFieldRenderer,
|
||||
)
|
||||
from django.forms import (
|
||||
CheckboxInput, CheckboxSelectMultiple, ClearableFileInput, RadioSelect,
|
||||
SelectDateWidget,
|
||||
)
|
||||
|
||||
|
||||
class FieldRenderer(BaseFieldRenderer):
|
||||
# Local application of https://github.com/zostera/django-bootstrap3/pull/859
|
||||
|
||||
def post_widget_render(self, html):
|
||||
if isinstance(self.widget, CheckboxSelectMultiple):
|
||||
html = self.list_to_class(html, "checkbox")
|
||||
elif isinstance(self.widget, RadioSelect):
|
||||
html = self.list_to_class(html, "radio")
|
||||
elif isinstance(self.widget, SelectDateWidget):
|
||||
html = self.fix_date_select_input(html)
|
||||
elif isinstance(self.widget, ClearableFileInput):
|
||||
html = self.fix_clearable_file_input(html)
|
||||
elif isinstance(self.widget, CheckboxInput):
|
||||
html = self.put_inside_label(html)
|
||||
return html
|
||||
|
||||
|
||||
class InlineFieldRenderer(BaseInlineFieldRenderer):
|
||||
# Local application of https://github.com/zostera/django-bootstrap3/pull/859
|
||||
|
||||
def post_widget_render(self, html):
|
||||
if isinstance(self.widget, CheckboxSelectMultiple):
|
||||
html = self.list_to_class(html, "checkbox")
|
||||
elif isinstance(self.widget, RadioSelect):
|
||||
html = self.list_to_class(html, "radio")
|
||||
elif isinstance(self.widget, SelectDateWidget):
|
||||
html = self.fix_date_select_input(html)
|
||||
elif isinstance(self.widget, ClearableFileInput):
|
||||
html = self.fix_clearable_file_input(html)
|
||||
elif isinstance(self.widget, CheckboxInput):
|
||||
html = self.put_inside_label(html)
|
||||
return html
|
||||
@@ -33,7 +33,7 @@
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import os
|
||||
from datetime import date
|
||||
from datetime import datetime
|
||||
|
||||
from django import forms
|
||||
from django.utils.formats import get_format
|
||||
@@ -188,11 +188,11 @@ class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
|
||||
time_attrs['autocomplete'] = 'off'
|
||||
if min_date:
|
||||
date_attrs['data-min'] = (
|
||||
min_date if isinstance(min_date, date) else min_date.astimezone(get_current_timezone()).date()
|
||||
min_date if not isinstance(min_date, datetime) else min_date.astimezone(get_current_timezone()).date()
|
||||
).isoformat()
|
||||
if max_date:
|
||||
date_attrs['data-max'] = (
|
||||
max_date if isinstance(max_date, date) else max_date.astimezone(get_current_timezone()).date()
|
||||
max_date if not isinstance(max_date, datetime) else max_date.astimezone(get_current_timezone()).date()
|
||||
).isoformat()
|
||||
|
||||
def date_placeholder():
|
||||
@@ -209,7 +209,10 @@ class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
|
||||
|
||||
date_attrs['placeholder'] = lazy(date_placeholder, str)
|
||||
time_attrs['placeholder'] = lazy(time_placeholder, str)
|
||||
|
||||
date_attrs['aria-label'] = _('Date')
|
||||
time_attrs['aria-label'] = _('Time')
|
||||
if 'aria-label' in attrs:
|
||||
del attrs['aria-label']
|
||||
widgets = (
|
||||
forms.DateInput(attrs=date_attrs, format=date_format),
|
||||
forms.TimeInput(attrs=time_attrs, format=time_format),
|
||||
|
||||
@@ -182,7 +182,7 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
|
||||
pdfmetrics.registerFontFamily('OpenSans', normal='OpenSans', bold='OpenSansBd',
|
||||
italic='OpenSansIt', boldItalic='OpenSansBI')
|
||||
|
||||
for family, styles in get_fonts().items():
|
||||
for family, styles in get_fonts(event=self.event, pdf_support_required=True).items():
|
||||
if family == self.event.settings.invoice_renderer_font:
|
||||
pdfmetrics.registerFont(TTFont(family, finders.find(styles['regular']['truetype'])))
|
||||
self.font_regular = family
|
||||
@@ -625,7 +625,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
)]
|
||||
else:
|
||||
tdata = [(
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['BoldRight']),
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']),
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Amount')), self.stylesheet['BoldRightNoSplit']),
|
||||
)]
|
||||
@@ -855,7 +855,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
|
||||
class Modern1Renderer(ClassicInvoiceRenderer):
|
||||
identifier = 'modern1'
|
||||
verbose_name = gettext_lazy('Modern Invoice Renderer (pretix 2.7)')
|
||||
verbose_name = gettext_lazy('Default invoice renderer (European-style letter)')
|
||||
bottom_margin = 16.9 * mm
|
||||
top_margin = 16.9 * mm
|
||||
right_margin = 20 * mm
|
||||
@@ -989,6 +989,37 @@ class Modern1Renderer(ClassicInvoiceRenderer):
|
||||
canvas.drawText(textobject)
|
||||
|
||||
|
||||
class Modern1SimplifiedRenderer(Modern1Renderer):
|
||||
identifier = 'modern1simplified'
|
||||
verbose_name = gettext_lazy('Simplified invoice renderer')
|
||||
|
||||
logo_left = Modern1Renderer.left_margin
|
||||
logo_width = pagesizes.A4[0] - Modern1Renderer.right_margin - logo_left
|
||||
logo_height = 25 * mm
|
||||
logo_top = 13 * mm
|
||||
logo_anchor = 'nw'
|
||||
|
||||
def _draw_invoice_from(self, canvas):
|
||||
super(Modern1Renderer, self)._draw_invoice_from(canvas)
|
||||
|
||||
def _draw_event(self, canvas):
|
||||
pass
|
||||
|
||||
def _get_intro(self):
|
||||
i = []
|
||||
|
||||
if not self.invoice.event.has_subevents and self.invoice.event.settings.show_dates_on_frontpage:
|
||||
i.append(Paragraph(
|
||||
pgettext('invoice', 'Event date: {date_range}').format(
|
||||
date_range=self.invoice.event.get_date_range_display(),
|
||||
),
|
||||
self.stylesheet['Normal'],
|
||||
))
|
||||
i.append(Spacer(2 * mm, 2 * mm))
|
||||
|
||||
return i + super()._get_intro()
|
||||
|
||||
|
||||
@receiver(register_invoice_renderers, dispatch_uid="invoice_renderer_classic")
|
||||
def recv_classic(sender, **kwargs):
|
||||
return [ClassicInvoiceRenderer, Modern1Renderer]
|
||||
return [ClassicInvoiceRenderer, Modern1Renderer, Modern1SimplifiedRenderer]
|
||||
|
||||
@@ -104,7 +104,7 @@ class Command(BaseCommand):
|
||||
with language(locale), override(timezone):
|
||||
for receiver, response in signal_result:
|
||||
if not response:
|
||||
return None
|
||||
continue
|
||||
ex = response(e, o, report_status)
|
||||
if ex.identifier == options['export_provider']:
|
||||
params = json.loads(options.get('parameters') or '{}')
|
||||
|
||||
@@ -268,7 +268,10 @@ def metric_values():
|
||||
dkey = key.decode("utf-8")
|
||||
splitted = dkey.split("{", 2)
|
||||
value = float(value.decode("utf-8"))
|
||||
metrics[splitted[0]]["{" + splitted[1]] = value
|
||||
if len(splitted) == 1:
|
||||
metrics[splitted[0]][""] = value
|
||||
else:
|
||||
metrics[splitted[0]]["{" + splitted[1]] = value
|
||||
|
||||
# Aliases
|
||||
aliases = {
|
||||
@@ -314,3 +317,5 @@ pretix_task_runs_total = Counter("pretix_task_runs_total", "Total calls to a cel
|
||||
["task_name", "status"])
|
||||
pretix_task_duration_seconds = Histogram("pretix_task_duration_seconds", "Call time of a celery task",
|
||||
["task_name"])
|
||||
pretix_successful_logins = Counter("pretix_logins_successful", "Successful logins", [])
|
||||
pretix_failed_logins = Counter("pretix_logins_failed", "Failed logins", ["reason"])
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user