forked from CGM_Public/pretix_original
Compare commits
597 Commits
release/20
...
fix-tests
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43b9049a12 | ||
|
|
7b16dfefbc | ||
|
|
8a5b13dee9 | ||
|
|
d7dde8c23e | ||
|
|
1e2f93fbc5 | ||
|
|
493fc03686 | ||
|
|
8d6d885f6e | ||
|
|
2aa989c293 | ||
|
|
06b226f40f | ||
|
|
73038b0d97 | ||
|
|
4513e31f0d | ||
|
|
d34175114b | ||
|
|
b2c71b47ce | ||
|
|
a889abc52b | ||
|
|
8c01b2a469 | ||
|
|
720c7fd7bb | ||
|
|
6ae6eba4de | ||
|
|
a173e347ea | ||
|
|
94d13e4cdd | ||
|
|
e618441231 | ||
|
|
cd57f1f024 | ||
|
|
075b9c187f | ||
|
|
d9f46cb817 | ||
|
|
2892d16861 | ||
|
|
9128624d68 | ||
|
|
d2cf8f801d | ||
|
|
682d0f886d | ||
|
|
d2cbd41a19 | ||
|
|
828f4e3168 | ||
|
|
e691afdd34 | ||
|
|
add90b08ec | ||
|
|
4539c6523b | ||
|
|
3453818c16 | ||
|
|
e5725d6d33 | ||
|
|
0cda1aeaaf | ||
|
|
769c451bcb | ||
|
|
5b60928205 | ||
|
|
b05f3a449d | ||
|
|
3262f2ba84 | ||
|
|
fe9d8e58a1 | ||
|
|
6706dbb4db | ||
|
|
87632ff8c5 | ||
|
|
2278dbdc4a | ||
|
|
e4e6cc0fcc | ||
|
|
ea73977c37 | ||
|
|
4fb5c6bef0 | ||
|
|
95511b0330 | ||
|
|
3340599aec | ||
|
|
e9a52d07d1 | ||
|
|
0a00a35ab1 | ||
|
|
24d8dc6c76 | ||
|
|
a9d48eaafe | ||
|
|
bb959fa494 | ||
|
|
6126309429 | ||
|
|
29b49ca82f | ||
|
|
7b7b83cb3e | ||
|
|
6bb2b3425d | ||
|
|
182d30ffb7 | ||
|
|
844c291575 | ||
|
|
ecdb1a8e09 | ||
|
|
6c6e5d5af6 | ||
|
|
c33853173b | ||
|
|
5f90bf80b8 | ||
|
|
0ba246846b | ||
|
|
88ea04551e | ||
|
|
2bfd8e17b0 | ||
|
|
1379e5f723 | ||
|
|
9fcaa3d730 | ||
|
|
22d92025c9 | ||
|
|
2b84c8d7a7 | ||
|
|
25ba2f1145 | ||
|
|
ae8ff60964 | ||
|
|
9fe6916ab5 | ||
|
|
634263f1ba | ||
|
|
67265e94a0 | ||
|
|
0fa2e9b5dd | ||
|
|
c99d93a078 | ||
|
|
9e20fac0da | ||
|
|
3e4ccc53be | ||
|
|
ce88dfa530 | ||
|
|
f0a06cd9fe | ||
|
|
7672e6274d | ||
|
|
061f578b29 | ||
|
|
79f8501a09 | ||
|
|
c5237b5021 | ||
|
|
0d6f7e74a3 | ||
|
|
21bd4a86a7 | ||
|
|
750f641018 | ||
|
|
2a385d14c4 | ||
|
|
6a7ab1bdf5 | ||
|
|
a73c4ad937 | ||
|
|
043e2eb9cf | ||
|
|
c0fb93ea3b | ||
|
|
4f9297e7d8 | ||
|
|
70b48fdd4b | ||
|
|
e7b5317431 | ||
|
|
63ef2e70e2 | ||
|
|
c4db2a48b6 | ||
|
|
de255b021e | ||
|
|
d3fce71b7f | ||
|
|
37dea068ce | ||
|
|
b5ef49cd3c | ||
|
|
245c5972c6 | ||
|
|
6597977752 | ||
|
|
580137577e | ||
|
|
a09550ce02 | ||
|
|
1b444b780d | ||
|
|
f41d8bb761 | ||
|
|
365dbe7a14 | ||
|
|
bf6078efb6 | ||
|
|
695b9a2ed6 | ||
|
|
25a15069ed | ||
|
|
11a4ea7b77 | ||
|
|
4e5e7df201 | ||
|
|
8f515aa327 | ||
|
|
dbc7fda2f8 | ||
|
|
5e686674ae | ||
|
|
5660cd7f93 | ||
|
|
a7a33ed165 | ||
|
|
9ffdf979f4 | ||
|
|
7338381e58 | ||
|
|
ce06672334 | ||
|
|
223f095611 | ||
|
|
b625dc9ec8 | ||
|
|
50c4a1c376 | ||
|
|
6fd2e42426 | ||
|
|
da651df4f0 | ||
|
|
a6f527e32d | ||
|
|
3ea61fbd1f | ||
|
|
6a959d4220 | ||
|
|
92959dbb1f | ||
|
|
6d6f3c4af8 | ||
|
|
8d14a285ca | ||
|
|
a6b8cd8a54 | ||
|
|
cb95cdc6ce | ||
|
|
cc7b00e206 | ||
|
|
136c54b9a8 | ||
|
|
28ba434e45 | ||
|
|
09320093ad | ||
|
|
89cfab6cad | ||
|
|
a180ce4c51 | ||
|
|
1200274ebf | ||
|
|
877401d8c0 | ||
|
|
44170c1b93 | ||
|
|
ce34bd0a13 | ||
|
|
b58f05efd0 | ||
|
|
3ac70e6e3a | ||
|
|
90123d6a58 | ||
|
|
48a3984db6 | ||
|
|
fe6ee4437f | ||
|
|
f470389cd8 | ||
|
|
c848594c21 | ||
|
|
e9a95b0b09 | ||
|
|
3b48b0782d | ||
|
|
b55bd8f75a | ||
|
|
39caadb335 | ||
|
|
dd6ebd7a48 | ||
|
|
ab576bb643 | ||
|
|
8bc16af36e | ||
|
|
537044bdc8 | ||
|
|
1ac7d03bb8 | ||
|
|
b939fad1c0 | ||
|
|
fb3a608c54 | ||
|
|
ebda10542e | ||
|
|
93dd6bf34d | ||
|
|
52148ebb7a | ||
|
|
82b4fe2733 | ||
|
|
79750e4f4b | ||
|
|
dbabbf7aab | ||
|
|
6158a1f2a4 | ||
|
|
68f6f921b5 | ||
|
|
d0e672435a | ||
|
|
5bc622bcfe | ||
|
|
d0184c1f48 | ||
|
|
e28bbb7ea0 | ||
|
|
fe54a42fc7 | ||
|
|
7365f165ad | ||
|
|
90ce802a33 | ||
|
|
d463878514 | ||
|
|
dd7ee84d29 | ||
|
|
f1e2d1f44c | ||
|
|
87a6a58f32 | ||
|
|
b2dd56bd41 | ||
|
|
f0ceab2305 | ||
|
|
14e3316dd9 | ||
|
|
eb28fdcba9 | ||
|
|
49e4a0faa0 | ||
|
|
e486089590 | ||
|
|
4bb02d4ad9 | ||
|
|
8ad852d9cb | ||
|
|
868fcfc471 | ||
|
|
d847c9a095 | ||
|
|
d1c6b22624 | ||
|
|
842987f48e | ||
|
|
58fc13ed91 | ||
|
|
8010d2e6bb | ||
|
|
1566f54764 | ||
|
|
9d380557e1 | ||
|
|
5758e0dd68 | ||
|
|
b4629e24a5 | ||
|
|
27f5121211 | ||
|
|
a5007e4bd6 | ||
|
|
fb3046210b | ||
|
|
37908bd042 | ||
|
|
74bcbe8f07 | ||
|
|
29f378c58b | ||
|
|
9b537aeb5c | ||
|
|
4b5cd35a0e | ||
|
|
e0675233d5 | ||
|
|
e9726a5227 | ||
|
|
78b65d0757 | ||
|
|
ef620ceb37 | ||
|
|
8575a5f1cd | ||
|
|
5762ffc035 | ||
|
|
79286bb051 | ||
|
|
05a2f411db | ||
|
|
e93e5c047c | ||
|
|
2619a658c9 | ||
|
|
808775c76b | ||
|
|
9f297fbd25 | ||
|
|
d882da0adb | ||
|
|
73bd4a746e | ||
|
|
bc5d0763f3 | ||
|
|
ff084f04b1 | ||
|
|
71af40a08b | ||
|
|
ef60093bae | ||
|
|
49c4cc639f | ||
|
|
e2800019f6 | ||
|
|
c44ea8aa81 | ||
|
|
47a03e1b2a | ||
|
|
86ddca15ca | ||
|
|
294b3966b0 | ||
|
|
fecc00231b | ||
|
|
b3dfc459f5 | ||
|
|
e21d63a7be | ||
|
|
9a807df158 | ||
|
|
e95d551711 | ||
|
|
7188e44fe5 | ||
|
|
a6a93555b6 | ||
|
|
94eb473e42 | ||
|
|
cbc3a344c1 | ||
|
|
47db52d75f | ||
|
|
ba99fe597c | ||
|
|
b638c00952 | ||
|
|
bfcca7046a | ||
|
|
ad5d10ff67 | ||
|
|
54d327deea | ||
|
|
d6505f946f | ||
|
|
9c4efa7dcf | ||
|
|
e6d26c4962 | ||
|
|
7ddbbe21f7 | ||
|
|
8d5ad0bd9e | ||
|
|
aff6a6f022 | ||
|
|
46008818ce | ||
|
|
95db04bad2 | ||
|
|
d0c62ec1cf | ||
|
|
d6cbb130bd | ||
|
|
097d2fcda0 | ||
|
|
41a7c13970 | ||
|
|
1b725810dd | ||
|
|
251f486480 | ||
|
|
a7afcdf753 | ||
|
|
0722341073 | ||
|
|
207bf101b8 | ||
|
|
e8f7cea1bf | ||
|
|
aa55eb2de2 | ||
|
|
9dc5c1b266 | ||
|
|
514f1def4d | ||
|
|
c2bc97a0d8 | ||
|
|
7fba473426 | ||
|
|
be87ba0000 | ||
|
|
76b7643c39 | ||
|
|
b1a3963b33 | ||
|
|
586e694ff3 | ||
|
|
f4383c67a4 | ||
|
|
46b2214836 | ||
|
|
0e20d897d2 | ||
|
|
0c09cccd4f | ||
|
|
5ca0833db1 | ||
|
|
7a63498333 | ||
|
|
b8c0887f79 | ||
|
|
9da65f60d7 | ||
|
|
806124304a | ||
|
|
0d57673a47 | ||
|
|
166b5e4f3b | ||
|
|
541b8f5bd6 | ||
|
|
d2b96b2425 | ||
|
|
04d4c4f8f1 | ||
|
|
f4da94cbcd | ||
|
|
97e3d5387f | ||
|
|
8fc07523a9 | ||
|
|
f18b0ae187 | ||
|
|
f7e16f56ac | ||
|
|
3f4e869cea | ||
|
|
8c2a1d58f4 | ||
|
|
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 |
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') }}
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
before_script:
|
||||
tests:
|
||||
image:
|
||||
name: pretix/ci-image
|
||||
stage: test
|
||||
before_script:
|
||||
- pip install -U pip uv
|
||||
- uv pip install --system -U wheel setuptools
|
||||
script:
|
||||
- virtualenv env
|
||||
- source env/bin/activate
|
||||
- pip install -U pip wheel setuptools
|
||||
- XDG_CACHE_HOME=/cache pip3 install -e ".[dev]"
|
||||
- uv pip install --system -e ".[dev]"
|
||||
- cd src
|
||||
- python manage.py check
|
||||
- make all compress
|
||||
- py.test --reruns 3 -n 3 tests
|
||||
tags:
|
||||
- python3
|
||||
- PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg py.test --reruns 3 -n 3 tests --maxfail=100
|
||||
except:
|
||||
- pypi
|
||||
pypi:
|
||||
stage: release
|
||||
image:
|
||||
name: pretix/ci-image
|
||||
before_script:
|
||||
- cat $PYPIRC > ~/.pypirc
|
||||
- pip install -U pip uv
|
||||
- uv pip install --system -U wheel setuptools twine build pretix-plugin-build check-manifest
|
||||
script:
|
||||
- cp /keys/.pypirc ~/.pypirc
|
||||
- virtualenv env
|
||||
- source env/bin/activate
|
||||
- pip install -U pip wheel setuptools check-manifest twine
|
||||
- XDG_CACHE_HOME=/cache pip3 install -e ".[dev]"
|
||||
- uv pip install --system -e ".[dev]"
|
||||
- python setup.py sdist
|
||||
- pip install dist/pretix-*.tar.gz
|
||||
- uv pip install --system dist/pretix-*.tar.gz
|
||||
- python -m pretix migrate
|
||||
- python -m pretix check
|
||||
- cd src
|
||||
@@ -33,13 +34,12 @@ pypi:
|
||||
- python -m build
|
||||
- twine check dist/*
|
||||
- twine upload dist/*
|
||||
tags:
|
||||
- python3
|
||||
only:
|
||||
- pypi
|
||||
artifacts:
|
||||
paths:
|
||||
- src/dist/
|
||||
|
||||
stages:
|
||||
- test
|
||||
- build
|
||||
|
||||
@@ -47,7 +47,7 @@ if [ "$1" == "taskworker" ]; then
|
||||
fi
|
||||
|
||||
if [ "$1" == "upgrade" ]; then
|
||||
exec python3 -m pretix updatestyles
|
||||
exec python3 -m pretix updateassets
|
||||
fi
|
||||
|
||||
exec python3 -m pretix "$@"
|
||||
|
||||
@@ -52,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``.
|
||||
@@ -89,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
|
||||
@@ -149,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
|
||||
@@ -169,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.
|
||||
|
||||
@@ -345,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``
|
||||
@@ -521,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
|
||||
|
||||
18
doc/admin/installation/community.rst
Normal file
18
doc/admin/installation/community.rst
Normal file
@@ -0,0 +1,18 @@
|
||||
.. 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>`_.
|
||||
|
||||
Docker
|
||||
------
|
||||
|
||||
- `docker compose setup <https://github.com/ZPascal/pretix-docker-compose>`_ by ZPascal
|
||||
@@ -19,7 +19,7 @@ You can use ``pip`` to update pretix directly to the development branch. Then, u
|
||||
(venv)$ pip3 install -U "git+https://github.com/pretix/pretix.git#egg=pretix"
|
||||
(venv)$ python -m pretix migrate
|
||||
(venv)$ python -m pretix rebuild
|
||||
(venv)$ python -m pretix updatestyles
|
||||
(venv)$ python -m pretix updateassets
|
||||
# systemctl restart pretix-web pretix-worker
|
||||
|
||||
Docker installation
|
||||
|
||||
@@ -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,11 +280,12 @@ 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
|
||||
(venv)$ python -m pretix rebuild
|
||||
(venv)$ python -m pretix updatestyles
|
||||
(venv)$ python -m pretix updateassets
|
||||
# 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. Pay special
|
||||
@@ -323,7 +325,7 @@ Then, proceed like after any plugin installation::
|
||||
|
||||
(venv)$ python -m pretix migrate
|
||||
(venv)$ python -m pretix rebuild
|
||||
(venv)$ python -m pretix updatestyles
|
||||
(venv)$ python -m pretix updateassets
|
||||
# systemctl restart pretix-web pretix-worker
|
||||
|
||||
.. _Postfix: https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-postfix-as-a-send-only-smtp-server-on-ubuntu-22-04
|
||||
|
||||
@@ -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_logins_successful
|
||||
Counter. The number of successful backend logins.
|
||||
|
||||
pretix_logins_failed
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -40,6 +40,11 @@ answers list of objects Answers to user
|
||||
seat objects The assigned seat (or ``null``)
|
||||
├ id integer Internal ID of the seat instance
|
||||
├ name string Human-readable seat name
|
||||
├ zone_name string Name of the zone the seat is in
|
||||
├ row_name string Name/number of the row the seat is in
|
||||
├ row_label string Additional label of the row (or ``null``)
|
||||
├ seat_number string Number of the seat within the row
|
||||
├ seat_label string Additional label of the seat (or ``null``)
|
||||
└ seat_guid string Identifier of the seat within the seating plan
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
@@ -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,
|
||||
…
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,12 @@ id integer Internal ID
|
||||
active boolean The discount will be ignored if this is ``false``
|
||||
internal_name string A name for the rule used in the backend
|
||||
position integer An integer, used for sorting the rules which are applied in order
|
||||
sales_channels list of strings Sales channels this discount is available on, such as
|
||||
``"web"`` or ``"resellers"``. Defaults to ``["web"]``.
|
||||
all_sales_channels boolean If ``true`` (default), the discount is available on all sales channels
|
||||
that support discounts.
|
||||
limit_sales_channels list of strings List of sales channel identifiers the discount is available on
|
||||
if ``all_sales_channels`` is ``false``.
|
||||
sales_channels list of strings **DEPRECATED.** Legacy interface, use ``all_sales_channels``
|
||||
and ``limit_sales_channels`` instead.
|
||||
available_from datetime The first date time at which this discount can be applied
|
||||
(or ``null``).
|
||||
available_until datetime The last date time at which this discount can be applied
|
||||
@@ -95,6 +99,8 @@ Endpoints
|
||||
"active": true,
|
||||
"internal_name": "3 for 2",
|
||||
"position": 1,
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
@@ -151,6 +157,8 @@ Endpoints
|
||||
"active": true,
|
||||
"internal_name": "3 for 2",
|
||||
"position": 1,
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
@@ -193,6 +201,8 @@ Endpoints
|
||||
"active": true,
|
||||
"internal_name": "3 for 2",
|
||||
"position": 1,
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
@@ -224,6 +234,8 @@ Endpoints
|
||||
"active": true,
|
||||
"internal_name": "3 for 2",
|
||||
"position": 1,
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
@@ -284,6 +296,8 @@ Endpoints
|
||||
"active": false,
|
||||
"internal_name": "3 for 2",
|
||||
"position": 1,
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
|
||||
@@ -49,8 +49,11 @@ item_meta_properties object Item-specific m
|
||||
valid_keys object Cryptographic keys for non-default signature schemes.
|
||||
For performance reason, value is omitted in lists and
|
||||
only contained in detail views. Value can be cached.
|
||||
sales_channels list A list of sales channels this event is available for
|
||||
sale on.
|
||||
all_sales_channels boolean If ``true`` (default), the event is available on all sales channels.
|
||||
limit_sales_channels list of strings List of sales channel identifiers the event is available on
|
||||
if ``all_sales_channels`` is ``false``.
|
||||
sales_channels list of strings **DEPRECATED.** Legacy interface, use ``all_sales_channels``
|
||||
and ``limit_sales_channels`` instead.
|
||||
public_url string The public, customer-facing URL of the event (read-only).
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
@@ -131,11 +134,13 @@ Endpoints
|
||||
"pretix.plugins.paypal",
|
||||
"pretix.plugins.ticketoutputpdf"
|
||||
],
|
||||
"sales_channels": [
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": [
|
||||
"web",
|
||||
"pretixpos",
|
||||
"resellers"
|
||||
],
|
||||
"sales_channels": [],
|
||||
"public_url": "https://pretix.eu/bigevents/sampleconf/"
|
||||
}
|
||||
]
|
||||
@@ -225,6 +230,8 @@ Endpoints
|
||||
"LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUNvd0JRWURLMlZ3QXlFQTdBRDcvdkZBMzNFc1k0ejJQSHI3aVpQc1o4bjVkaDBhalA4Z3l6Tm1tSXM9Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo="
|
||||
]
|
||||
},
|
||||
"all_sales_channels": true,
|
||||
"limit_sales_channels": [],
|
||||
"sales_channels": [
|
||||
"web",
|
||||
"pretixpos",
|
||||
@@ -282,11 +289,8 @@ Endpoints
|
||||
"pretix.plugins.stripe",
|
||||
"pretix.plugins.paypal"
|
||||
],
|
||||
"sales_channels": [
|
||||
"web",
|
||||
"pretixpos",
|
||||
"resellers"
|
||||
]
|
||||
"all_sales_channels": true,
|
||||
"limit_sales_channels": []
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@@ -322,6 +326,8 @@ Endpoints
|
||||
"pretix.plugins.stripe",
|
||||
"pretix.plugins.paypal"
|
||||
],
|
||||
"all_sales_channels": true,
|
||||
"limit_sales_channels": [],
|
||||
"sales_channels": [
|
||||
"web",
|
||||
"pretixpos",
|
||||
@@ -387,11 +393,8 @@ Endpoints
|
||||
"pretix.plugins.stripe",
|
||||
"pretix.plugins.paypal"
|
||||
],
|
||||
"sales_channels": [
|
||||
"web",
|
||||
"pretixpos",
|
||||
"resellers"
|
||||
]
|
||||
"all_sales_channels": true,
|
||||
"limit_sales_channels": []
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@@ -427,6 +430,8 @@ Endpoints
|
||||
"pretix.plugins.stripe",
|
||||
"pretix.plugins.paypal"
|
||||
],
|
||||
"all_sales_channels": true,
|
||||
"limit_sales_channels": [],
|
||||
"sales_channels": [
|
||||
"web",
|
||||
"pretixpos",
|
||||
@@ -502,6 +507,8 @@ Endpoints
|
||||
"pretix.plugins.paypal",
|
||||
"pretix.plugins.pretixdroid"
|
||||
],
|
||||
"all_sales_channels": true,
|
||||
"limit_sales_channels": [],
|
||||
"sales_channels": [
|
||||
"web",
|
||||
"pretixpos",
|
||||
|
||||
@@ -96,6 +96,8 @@ Endpoints
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:query string secret: Only show gift cards with the given secret.
|
||||
:query string value: Only show gift cards with the given value.
|
||||
:query boolean expired: Filter for gift cards that are (not) expired.
|
||||
:query boolean testmode: Filter for gift cards that are (not) in test mode.
|
||||
:query boolean include_accepted: Also show gift cards issued by other organizers that are accepted by this organizer.
|
||||
:query string expand: If you pass ``"owner_ticket"``, the respective field will be shown as a nested value instead of just an ID.
|
||||
|
||||
@@ -30,6 +30,7 @@ at :ref:`plugin-docs`.
|
||||
checkinlists
|
||||
waitinglist
|
||||
customers
|
||||
saleschannels
|
||||
membershiptypes
|
||||
memberships
|
||||
giftcards
|
||||
|
||||
@@ -38,15 +38,26 @@ require_membership boolean If ``true``, bo
|
||||
require_membership_hidden boolean If ``true`` and ``require_membership`` is set, this variation will
|
||||
be hidden from users without a valid membership.
|
||||
require_membership_types list of integers Internal IDs of membership types valid if ``require_membership`` is ``true``
|
||||
sales_channels list of strings Sales channels this variation is available on, such as
|
||||
``"web"`` or ``"resellers"``. Defaults to all existing sales channels.
|
||||
all_sales_channels boolean If ``true`` (default), the variation is available on all sales channels.
|
||||
limit_sales_channels list of strings List of sales channel identifiers the variation is available on
|
||||
if ``all_sales_channels`` is ``false``.
|
||||
The item-level list takes precedence, i.e. a sales
|
||||
channel needs to be on both lists for the item to be
|
||||
available.
|
||||
channel needs to be on both lists for the variation to be
|
||||
available (unless ``all_sales_channels`` is used).
|
||||
sales_channels list of strings **DEPRECATED.** Legacy interface, use ``all_sales_channels``
|
||||
and ``limit_sales_channels`` instead.
|
||||
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.
|
||||
@@ -103,9 +114,13 @@ Endpoints
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": false,
|
||||
"description": {
|
||||
"en": "Test2"
|
||||
@@ -129,9 +144,13 @@ Endpoints
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": false,
|
||||
"description": {},
|
||||
"position": 1,
|
||||
@@ -145,6 +164,7 @@ Endpoints
|
||||
}
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:query string search: Filter the list by the value of the variation (substring search).
|
||||
:query boolean active: If set to ``true`` or ``false``, only items with this value for the field ``active`` will be
|
||||
returned.
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
@@ -190,9 +210,13 @@ Endpoints
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"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,
|
||||
@@ -230,9 +254,12 @@ Endpoints
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"all_sales_channels": false,
|
||||
"limit_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,
|
||||
@@ -261,9 +288,13 @@ Endpoints
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"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,
|
||||
@@ -323,9 +354,13 @@ Endpoints
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"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,
|
||||
|
||||
@@ -46,12 +46,23 @@ personalized boolean ``true`` for
|
||||
position integer An integer, used for sorting
|
||||
picture file A product picture to be displayed in the shop
|
||||
(can be ``null``).
|
||||
sales_channels list of strings Sales channels this product is available on, such as
|
||||
``"web"`` or ``"resellers"``. Defaults to ``["web"]``.
|
||||
all_sales_channels boolean If ``true`` (default), the item is available on all sales channels.
|
||||
limit_sales_channels list of strings List of sales channel identifiers the item is available on
|
||||
if ``all_sales_channels`` is ``false``.
|
||||
sales_channels list of strings **DEPRECATED.** Legacy interface, use ``all_sales_channels``
|
||||
and ``limit_sales_channels`` instead.
|
||||
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``).
|
||||
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.
|
||||
@@ -149,15 +160,26 @@ variations list of objects A list with o
|
||||
be hidden from users without a valid membership.
|
||||
├ require_membership_types list of integers Internal IDs of membership types valid if ``require_membership`` is ``true``
|
||||
Markdown syntax or can be ``null``.
|
||||
├ sales_channels list of strings Sales channels this variation is available on, such as
|
||||
``"web"`` or ``"resellers"``. Defaults to all existing sales channels.
|
||||
├ all_sales_channels boolean If ``true`` (default), the variation is available on all sales channels.
|
||||
├ limit_sales_channels list of strings List of sales channel identifiers the variation is available on
|
||||
if ``all_sales_channels`` is ``false``.
|
||||
The item-level list takes precedence, i.e. a sales
|
||||
channel needs to be on both lists for the item to be
|
||||
available.
|
||||
channel needs to be on both lists for the variation to be
|
||||
available (unless ``all_sales_channels`` is used).
|
||||
├ sales_channels list of strings **DEPRECATED.** Legacy interface, use ``all_sales_channels``
|
||||
and ``limit_sales_channels`` instead.
|
||||
├ 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.
|
||||
@@ -260,6 +282,8 @@ Endpoints
|
||||
"id": 1,
|
||||
"name": {"en": "Standard ticket"},
|
||||
"internal_name": "",
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"default_price": "23.00",
|
||||
"original_price": null,
|
||||
@@ -279,7 +303,9 @@ 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,
|
||||
@@ -322,9 +348,13 @@ Endpoints
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"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": {},
|
||||
@@ -342,9 +372,13 @@ Endpoints
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"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": {},
|
||||
@@ -358,6 +392,7 @@ Endpoints
|
||||
}
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:query string search: Filter the list by internal name or name of the item (substring search).
|
||||
:query boolean active: If set to ``true`` or ``false``, only items with this value for the field ``active`` will be
|
||||
returned.
|
||||
:query integer category: If set to the ID of a category, only items within that category will be returned.
|
||||
@@ -398,6 +433,8 @@ Endpoints
|
||||
"id": 1,
|
||||
"name": {"en": "Standard ticket"},
|
||||
"internal_name": "",
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"default_price": "23.00",
|
||||
"original_price": null,
|
||||
@@ -417,7 +454,9 @@ 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,
|
||||
@@ -461,9 +500,13 @@ Endpoints
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"description": null,
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"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
|
||||
@@ -480,9 +523,13 @@ Endpoints
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"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": {},
|
||||
@@ -517,7 +564,8 @@ Endpoints
|
||||
"id": 1,
|
||||
"name": {"en": "Standard ticket"},
|
||||
"internal_name": "",
|
||||
"sales_channels": ["web"],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"default_price": "23.00",
|
||||
"original_price": null,
|
||||
"category": null,
|
||||
@@ -536,7 +584,9 @@ 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,
|
||||
@@ -578,9 +628,12 @@ Endpoints
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"all_sales_channels": false,
|
||||
"limit_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": {},
|
||||
@@ -598,9 +651,12 @@ Endpoints
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"all_sales_channels": false,
|
||||
"limit_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": {},
|
||||
@@ -623,6 +679,8 @@ Endpoints
|
||||
"id": 1,
|
||||
"name": {"en": "Standard ticket"},
|
||||
"internal_name": "",
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"default_price": "23.00",
|
||||
"original_price": null,
|
||||
@@ -642,7 +700,9 @@ 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,
|
||||
@@ -685,9 +745,13 @@ Endpoints
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"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": {},
|
||||
@@ -705,9 +769,13 @@ Endpoints
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"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": {},
|
||||
@@ -761,6 +829,8 @@ Endpoints
|
||||
"id": 1,
|
||||
"name": {"en": "Ticket"},
|
||||
"internal_name": "",
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"default_price": "25.00",
|
||||
"original_price": null,
|
||||
@@ -780,7 +850,9 @@ 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,
|
||||
@@ -823,9 +895,13 @@ Endpoints
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"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": {},
|
||||
@@ -843,9 +919,13 @@ Endpoints
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"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": {},
|
||||
|
||||
@@ -179,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``)
|
||||
@@ -210,6 +215,11 @@ answers list of objects Answers to user
|
||||
seat objects The assigned seat. Can be ``null``.
|
||||
├ id integer Internal ID of the seat instance
|
||||
├ name string Human-readable seat name
|
||||
├ zone_name string Name of the zone the seat is in
|
||||
├ row_name string Name/number of the row the seat is in
|
||||
├ row_label string Additional label of the row (or ``null``)
|
||||
├ seat_number string Number of the seat within the row
|
||||
├ seat_label string Additional label of the seat (or ``null``)
|
||||
└ seat_guid string Identifier of the seat within the seating plan
|
||||
pdf_data object Data object required for ticket PDF generation. By default,
|
||||
this field is missing. It will be added only if you add the
|
||||
@@ -367,6 +377,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,
|
||||
@@ -449,10 +460,13 @@ List of all orders
|
||||
:query datetime modified_since: Only return orders that have changed since the given date. Be careful: We only
|
||||
recommend using this in combination with ``testmode=false``, since test mode orders can vanish at any time and
|
||||
you will not notice it using this method.
|
||||
:query datetime created_since: Only return orders that have been created since the given date.
|
||||
:query datetime created_since: Only return orders that have been created since the given date (inclusive).
|
||||
:query datetime created_before: Only return orders that have been created before the given date (exclusive).
|
||||
:query integer subevent: Only return orders with a position that contains this subevent ID. *Warning:* Result will also include orders if they contain mixed subevents, and it will even return orders where the subevent is only contained in a canceled position.
|
||||
:query datetime subevent_after: Only return orders that contain a ticket for a subevent taking place after the given date. This is an exclusive after, and it considers the **end** of the subevent (or its start, if the end is not set).
|
||||
:query datetime subevent_before: Only return orders that contain a ticket for a subevent taking place after the given date. This is an exclusive before, and it considers the **start** of the subevent.
|
||||
:query string sales_channel: Only return orders with the given sales channel identifier (e.g. ``"web"``).
|
||||
:query string payment_provider: Only return orders that contain a payment using the given payment provider. Note that this also searches for partial incomplete, or failed payments within the order and is not useful to get a sum of payment amounts without further processing.
|
||||
:query string exclude: Exclude a field from the output, e.g. ``fees`` or ``positions.downloads``. Can be used as a performance optimization. Can be passed multiple times.
|
||||
:query string include: Include only the given field in the output, e.g. ``fees`` or ``positions.downloads``. Can be used as a performance optimization. Can be passed multiple times. ``include`` is applied before ``exclude``, so ``exclude`` takes precedence.
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
@@ -589,6 +603,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",
|
||||
@@ -1541,6 +1556,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",
|
||||
@@ -1654,6 +1670,7 @@ Fetching individual positions
|
||||
},
|
||||
"attendee_email": null,
|
||||
"voucher": null,
|
||||
"voucher_budget_use": null,
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": null,
|
||||
"tax_value": "0.00",
|
||||
|
||||
219
doc/api/resources/saleschannels.rst
Normal file
219
doc/api/resources/saleschannels.rst
Normal file
@@ -0,0 +1,219 @@
|
||||
Sales channels
|
||||
==============
|
||||
|
||||
Resource description
|
||||
--------------------
|
||||
|
||||
The sales channel resource contains the following public fields:
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
===================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
identifier string Internal ID of the sales channel. For sales channel types
|
||||
that allow only one instance, this is the same as ``type``.
|
||||
For sales channel types that allow multiple instances, this
|
||||
is always prefixed with ``type.``.
|
||||
label multi-lingual string Human-readable name of the sales channel
|
||||
type string Type of the sales channel. Only channels with type ``api``
|
||||
can currently be created through the API.
|
||||
position integer Position for sorting lists of sales channels
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/saleschannels/
|
||||
|
||||
Returns a list of all sales channels within a given organizer.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/saleschannels/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"identifier": "web",
|
||||
"name": {
|
||||
"en": "Online shop"
|
||||
},
|
||||
"type": "web",
|
||||
"position": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/saleschannels/(identifier)/
|
||||
|
||||
Returns information on one sales channel, identified by its identifier.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/saleschannels/web/ 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
|
||||
|
||||
{
|
||||
"identifier": "web",
|
||||
"name": {
|
||||
"en": "Online shop"
|
||||
},
|
||||
"type": "web",
|
||||
"position": 0
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param identifier: The ``identifier`` field of the sales channel 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)/saleschannels/
|
||||
|
||||
Creates a sales channel
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/saleschannels/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"identifier": "api.custom",
|
||||
"name": {
|
||||
"en": "Custom integration"
|
||||
},
|
||||
"type": "api",
|
||||
"position": 2
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"identifier": "api.custom",
|
||||
"name": {
|
||||
"en": "Custom integration"
|
||||
},
|
||||
"type": "api",
|
||||
"position": 2
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to create a sales channel for
|
||||
:statuscode 201: no error
|
||||
:statuscode 400: The sales channel 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)/saleschannels/(identifier)/
|
||||
|
||||
Update a sales channel. 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.
|
||||
|
||||
You can change all fields of the resource except the ``identifier`` and ``type`` fields.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/bigevents/saleschannels/web/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 94
|
||||
|
||||
{
|
||||
"position": 5
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"identifier": "web",
|
||||
"name": {
|
||||
"en": "Online shop"
|
||||
},
|
||||
"type": "web",
|
||||
"position": 5
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param identifier: The ``identifier`` field of the sales channel to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The sales channel 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)/saleschannels/(identifier)/
|
||||
|
||||
Delete a sales channel. You can not delete sales channels which have already been used or which are integral parts
|
||||
of the system.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v1/organizers/bigevents/saleschannels/api.custom/ 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 identifier: The ``identifier`` field of the sales channel 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 **or** the sales channel is currently in use.
|
||||
@@ -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,
|
||||
...
|
||||
}
|
||||
|
||||
@@ -12,8 +12,9 @@ Core
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
: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
|
||||
item_copy_data, register_sales_channel_types, register_global_settings, quota_availability, global_email_filter,
|
||||
register_ticket_secret_generators, gift_card_transaction_display,
|
||||
register_text_placeholders, register_mail_placeholders
|
||||
|
||||
Order events
|
||||
""""""""""""
|
||||
@@ -34,7 +35,7 @@ Frontend
|
||||
--------
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
:members: html_head, html_footer, footer_link, global_footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, contact_form_fields_overrides, question_form_fields_overrides, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header
|
||||
:members: html_head, html_footer, footer_link, global_footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, contact_form_fields_overrides, question_form_fields_overrides, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header, seatingframe_html_head
|
||||
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ The project pretix is split into several components. The main components are:
|
||||
create and manage their events, items, orders and tickets.
|
||||
|
||||
**presale**
|
||||
This is the ticket-shop itself, containing all of the parts visible to the
|
||||
This is the ticket shop itself, containing all of the parts visible to the
|
||||
end user. Also called "frontend" in parts of this documentation.
|
||||
|
||||
**api**
|
||||
|
||||
@@ -19,3 +19,4 @@ Contents:
|
||||
permissions
|
||||
logging
|
||||
locking
|
||||
timemachine
|
||||
|
||||
@@ -15,7 +15,7 @@ includes serializers for serializing the following types:
|
||||
* Built-in types: ``int``, ``float``, ``decimal.Decimal``, ``dict``, ``list``, ``bool``
|
||||
* ``datetime.date``, ``datetime.datetime``, ``datetime.time``
|
||||
* ``LazyI18nString``
|
||||
* References to Django ``File`` objects that are already stored in a storage backend
|
||||
* References to Django ``File`` objects that are already stored in a storage backend [#f1]_
|
||||
* References to model instances
|
||||
|
||||
In code, we recommend to always use the ``.get()`` method on the settings object to access a value, but for
|
||||
@@ -55,6 +55,9 @@ You can simply use it like this:
|
||||
"preserve his reservation."),
|
||||
)
|
||||
|
||||
|
||||
.. _settings-defaults-in-plugins:
|
||||
|
||||
Defaults in plugins
|
||||
-------------------
|
||||
|
||||
@@ -70,3 +73,9 @@ Make sure that you include this code in a module that is imported at app loading
|
||||
|
||||
.. _django-hierarkey: https://github.com/raphaelm/django-hierarkey
|
||||
.. _documentation: https://django-hierarkey.readthedocs.io/en/latest/
|
||||
|
||||
.. rubric:: Footnotes
|
||||
|
||||
.. [#f1] If you store ``File`` instances in per-event settings, make sure to always register them with ``add_default``
|
||||
as described above in :ref:`settings-defaults-in-plugins`. Otherwise, the file won't get copied properly if the
|
||||
user copies the settings of an existing event to a new one.
|
||||
|
||||
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 updateassets
|
||||
|
||||
|
||||
.. _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.
|
||||
|
||||
@@ -41,6 +41,6 @@ pretix/
|
||||
|
||||
tests/
|
||||
This is the root directory for all test codes. It includes subdirectories ``api``, ``base``,
|
||||
``control``, ``presale``, ``helpers`, ``multidomain`` and ``plugins`` to mirror the structure
|
||||
``control``, ``presale``, ``helpers``, ``multidomain`` and ``plugins`` to mirror the structure
|
||||
of the pretix source code as well as ``testdummy``, which is a pretix plugin used during
|
||||
testing.
|
||||
|
||||
@@ -34,13 +34,19 @@ 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
|
||||
exhibitor_tags list of strings Internal tags to categorize exhibitors, not shown to exhibitor.
|
||||
The tags need to be created through the web interface currently.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
You can also access the scanned leads through the API which contains the following public fields:
|
||||
@@ -62,6 +68,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,13 +112,17 @@ 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,
|
||||
"comment": ""
|
||||
"comment": "",
|
||||
"exhibitor_tags": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -156,13 +167,17 @@ 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,
|
||||
"comment": ""
|
||||
"comment": "",
|
||||
"exhibitor_tags": []
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
@@ -357,12 +372,16 @@ 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,
|
||||
"allow_lead_access": true,
|
||||
"allow_voucher_access": true,
|
||||
"comment": ""
|
||||
"comment": "",
|
||||
"exhibitor_tags": [
|
||||
"Gold Sponsor"
|
||||
]
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@@ -386,13 +405,19 @@ 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,
|
||||
"comment": ""
|
||||
"comment": "",
|
||||
"exhibitor_tags": [
|
||||
"Gold Sponsor"
|
||||
]
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to create new exhibitor for
|
||||
@@ -444,13 +469,19 @@ 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,
|
||||
"comment": ""
|
||||
"comment": "",
|
||||
"exhibitor_tags": [
|
||||
"Gold Sponsor"
|
||||
]
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
@@ -561,6 +592,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 +628,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 +667,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 +702,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 +736,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 +793,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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
=========================== ==========================================================
|
||||
@@ -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::
|
||||
|
||||
@@ -339,9 +339,9 @@ Currently, the following attributes are understood by pretix itself:
|
||||
``data-attendee-name``, which will pre-fill the last part of the name, whatever that is.
|
||||
|
||||
* ``data-invoice-address-FIELD`` will pre-fill the corresponding field of the invoice address. Possible values for
|
||||
``FIELD`` are ``company``, ``street``, ``zipcode``, ``city`` and ``country``, as well as fields specified by the
|
||||
naming scheme such as ``name-title`` or ``name-given-name`` (see above). ``country`` expects a two-character
|
||||
country code.
|
||||
``FIELD`` are ``company``, ``street``, ``zipcode``, ``city``, ``country``, ``internal-reference``, ``vat-id``, and
|
||||
``custom-field``, as well as fields specified by the naming scheme such as ``name-title`` or ``name-given-name``
|
||||
(see above). ``country`` expects a two-character country code.
|
||||
|
||||
* If ``data-fix="true"`` is given, the user will not be able to change the other given values later. This currently
|
||||
only works for the order email address as well as the invoice address. Attendee-level fields and questions can
|
||||
@@ -429,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
|
||||
----------------
|
||||
|
||||
|
||||
@@ -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[argon2]==4.2.*",
|
||||
"django-bootstrap3==24.2",
|
||||
"django-compressor==4.5",
|
||||
"django-countries==7.6.*",
|
||||
"django-filter==24.2",
|
||||
"django-formset-js-improved==0.5.0.3",
|
||||
"django-formtools==2.4.1",
|
||||
"django-hierarkey==1.1.*",
|
||||
"django-hijack==3.3.*",
|
||||
"django-formtools==2.5.1",
|
||||
"django-hierarkey==1.2.*",
|
||||
"django-hijack==3.5.*",
|
||||
"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==7.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
||||
"importlib_metadata==8.*", # 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,27 +73,26 @@ dependencies = [
|
||||
"packaging",
|
||||
"paypalrestsdk==1.13.*",
|
||||
"paypal-checkout-serversdk==1.0.*",
|
||||
"PyJWT==2.7.*",
|
||||
"PyJWT==2.8.*",
|
||||
"phonenumberslite==8.13.*",
|
||||
"Pillow==9.5.*",
|
||||
"Pillow==10.4.*",
|
||||
"pretix-plugin-build",
|
||||
"protobuf==4.23.*",
|
||||
"protobuf==5.27.*",
|
||||
"psycopg2-binary",
|
||||
"pycountry",
|
||||
"pycparser==2.21",
|
||||
"pycryptodome==3.18.*",
|
||||
"pypdf==3.9.*",
|
||||
"pycparser==2.22",
|
||||
"pycryptodome==3.20.*",
|
||||
"pypdf==4.2.*",
|
||||
"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==2.5.*",
|
||||
"sepaxml==2.6.*",
|
||||
"slimit",
|
||||
"static3==0.7.*",
|
||||
@@ -101,35 +100,34 @@ dependencies = [
|
||||
"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.2.*",
|
||||
"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.23.*",
|
||||
"flake8==7.1.*",
|
||||
"freezegun",
|
||||
"isort==5.12.*",
|
||||
"pep8-naming==0.13.*",
|
||||
"isort==5.13.*",
|
||||
"pep8-naming==0.14.*",
|
||||
"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__ = "2024.1.0"
|
||||
__version__ = "2024.7.0.dev0"
|
||||
|
||||
@@ -79,6 +79,7 @@ ALL_LANGUAGES = [
|
||||
('de', _('German')),
|
||||
('de-informal', _('German (informal)')),
|
||||
('ar', _('Arabic')),
|
||||
('ca', _('Catalan')),
|
||||
('zh-hans', _('Chinese (simplified)')),
|
||||
('zh-hant', _('Chinese (traditional)')),
|
||||
('cs', _('Czech')),
|
||||
@@ -98,6 +99,8 @@ ALL_LANGUAGES = [
|
||||
('pt-br', _('Portuguese (Brazil)')),
|
||||
('ro', _('Romanian')),
|
||||
('ru', _('Russian')),
|
||||
('sk', _('Slovak')),
|
||||
('sv', _('Swedish')),
|
||||
('es', _('Spanish')),
|
||||
('tr', _('Turkish')),
|
||||
('uk', _('Ukrainian')),
|
||||
@@ -111,6 +114,7 @@ LANGUAGES_RTL = {
|
||||
LANGUAGES_INCUBATING = {
|
||||
'fi', 'pt-br', 'gl',
|
||||
}
|
||||
LANGUAGES = ALL_LANGUAGES
|
||||
LOCALE_PATHS = [
|
||||
os.path.join(os.path.dirname(__file__), 'locale'),
|
||||
]
|
||||
@@ -234,7 +238,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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
import json
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
|
||||
class AsymmetricField(serializers.Field):
|
||||
@@ -61,3 +62,57 @@ class CompatibleJSONField(serializers.JSONField):
|
||||
if value:
|
||||
return json.loads(value)
|
||||
return value
|
||||
|
||||
|
||||
class SalesChannelMigrationMixin:
|
||||
"""
|
||||
Translates between the old field "sales_channels" and the new field combo "all_sales_channels"/"limit_sales_channels".
|
||||
"""
|
||||
|
||||
@property
|
||||
def organizer(self):
|
||||
if "organizer" in self.context:
|
||||
return self.context["organizer"]
|
||||
elif "event" in self.context:
|
||||
return self.context["event"].organizer
|
||||
else:
|
||||
raise ValueError("organizer not in context")
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if "sales_channels" in data:
|
||||
all_channels = {
|
||||
s.identifier for s in
|
||||
self.organizer.sales_channels.all()
|
||||
}
|
||||
|
||||
if data.get("all_sales_channels") and set(data["sales_channels"]) != all_channels:
|
||||
raise ValidationError(
|
||||
"If 'all_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
|
||||
"the list of all sales channels."
|
||||
)
|
||||
|
||||
if data.get("limit_sales_channels") and set(data["sales_channels"]) != set(data["limit_sales_channels"]):
|
||||
raise ValidationError(
|
||||
"If 'limit_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
|
||||
"the same list."
|
||||
)
|
||||
|
||||
if data["sales_channels"] == all_channels:
|
||||
data["all_sales_channels"] = True
|
||||
data["limit_sales_channels"] = []
|
||||
else:
|
||||
data["all_sales_channels"] = False
|
||||
data["limit_sales_channels"] = data["sales_channels"]
|
||||
del data["sales_channels"]
|
||||
return super().to_internal_value(data)
|
||||
|
||||
def to_representation(self, value):
|
||||
value = super().to_representation(value)
|
||||
if value.get("all_sales_channels"):
|
||||
value["sales_channels"] = sorted([
|
||||
s.identifier for s in
|
||||
self.organizer.sales_channels.all()
|
||||
])
|
||||
else:
|
||||
value["sales_channels"] = value["limit_sales_channels"]
|
||||
return value
|
||||
|
||||
@@ -33,7 +33,7 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.order import (
|
||||
AnswerCreateSerializer, AnswerSerializer, InlineSeatSerializer,
|
||||
)
|
||||
from pretix.base.models import Seat, Voucher
|
||||
from pretix.base.models import SalesChannel, Seat, Voucher
|
||||
from pretix.base.models.orders import CartPosition
|
||||
|
||||
|
||||
@@ -212,7 +212,11 @@ class CartPositionCreateSerializer(BaseCartPositionCreateSerializer):
|
||||
addons = BaseCartPositionCreateSerializer(many=True, required=False)
|
||||
bundled = BaseCartPositionCreateSerializer(many=True, required=False)
|
||||
seat = serializers.CharField(required=False, allow_null=True)
|
||||
sales_channel = serializers.CharField(required=False, default='sales_channel')
|
||||
sales_channel = serializers.SlugRelatedField(
|
||||
slug_field='identifier',
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
)
|
||||
voucher = serializers.CharField(required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
@@ -221,6 +225,10 @@ class CartPositionCreateSerializer(BaseCartPositionCreateSerializer):
|
||||
'cart_id', 'expires', 'addons', 'bundled', 'seat', 'sales_channel', 'voucher'
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
|
||||
|
||||
def validate_cart_id(self, cid):
|
||||
if cid and not cid.endswith('@api'):
|
||||
raise ValidationError('Cart ID should end in @api or be empty.')
|
||||
|
||||
@@ -25,14 +25,20 @@ from rest_framework.exceptions import ValidationError
|
||||
|
||||
from pretix.api.serializers.event import SubEventSerializer
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.media import MEDIA_TYPES
|
||||
from pretix.base.models import Checkin, CheckinList
|
||||
from pretix.base.models import Checkin, CheckinList, SalesChannel
|
||||
|
||||
|
||||
class CheckinListSerializer(I18nAwareModelSerializer):
|
||||
checkin_count = serializers.IntegerField(read_only=True)
|
||||
position_count = serializers.IntegerField(read_only=True)
|
||||
auto_checkin_sales_channels = serializers.SlugRelatedField(
|
||||
slug_field="identifier",
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
allow_empty=True,
|
||||
many=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CheckinList
|
||||
@@ -43,6 +49,8 @@ class CheckinListSerializer(I18nAwareModelSerializer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['auto_checkin_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||
|
||||
if 'subevent' in self.context['request'].query_params.getlist('expand'):
|
||||
self.fields['subevent'] = SubEventSerializer(read_only=True)
|
||||
|
||||
@@ -72,10 +80,6 @@ class CheckinListSerializer(I18nAwareModelSerializer):
|
||||
if full_data.get('subevent'):
|
||||
raise ValidationError(_('The subevent does not belong to this event.'))
|
||||
|
||||
for channel in full_data.get('auto_checkin_sales_channels') or []:
|
||||
if channel not in get_all_sales_channels():
|
||||
raise ValidationError(_('Unknown sales channel.'))
|
||||
|
||||
CheckinList.validate_rules(data.get('rules'))
|
||||
|
||||
return data
|
||||
|
||||
@@ -19,18 +19,27 @@
|
||||
# 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 serializers
|
||||
|
||||
from pretix.api.serializers import SalesChannelMigrationMixin
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.models import Discount
|
||||
from pretix.base.models import Discount, SalesChannel
|
||||
|
||||
|
||||
class DiscountSerializer(I18nAwareModelSerializer):
|
||||
class DiscountSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
limit_sales_channels = serializers.SlugRelatedField(
|
||||
slug_field="identifier",
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
allow_empty=True,
|
||||
many=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Discount
|
||||
fields = ('id', 'active', 'internal_name', 'position', 'sales_channels', 'available_from',
|
||||
'available_until', 'subevent_mode', 'condition_all_products', 'condition_limit_products',
|
||||
'condition_apply_to_addons', 'condition_min_count', 'condition_min_value',
|
||||
fields = ('id', 'active', 'internal_name', 'position', 'all_sales_channels', 'limit_sales_channels',
|
||||
'available_from', 'available_until', 'subevent_mode', 'condition_all_products',
|
||||
'condition_limit_products', 'condition_apply_to_addons', 'condition_min_count', 'condition_min_value',
|
||||
'benefit_discount_matching_percent', 'benefit_only_apply_to_cheapest_n_matches',
|
||||
'benefit_same_products', 'benefit_limit_products', 'benefit_apply_to_addons',
|
||||
'benefit_ignore_voucher_discounted', 'condition_ignore_voucher_discounted')
|
||||
@@ -39,6 +48,7 @@ class DiscountSerializer(I18nAwareModelSerializer):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['condition_limit_products'].queryset = self.context['event'].items.all()
|
||||
self.fields['benefit_limit_products'].queryset = self.context['event'].items.all()
|
||||
self.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
|
||||
@@ -46,10 +46,14 @@ from rest_framework import serializers
|
||||
from rest_framework.fields import ChoiceField, Field
|
||||
from rest_framework.relations import SlugRelatedField
|
||||
|
||||
from pretix.api.serializers import CompatibleJSONField
|
||||
from pretix.api.serializers import (
|
||||
CompatibleJSONField, SalesChannelMigrationMixin,
|
||||
)
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.settings import SettingsSerializer
|
||||
from pretix.base.models import Device, Event, TaxRule, TeamAPIToken
|
||||
from pretix.base.models import (
|
||||
Device, Event, SalesChannel, TaxRule, TeamAPIToken,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.items import (
|
||||
ItemMetaProperty, SubEventItem, SubEventItemVariation,
|
||||
@@ -161,7 +165,7 @@ class ValidKeysField(Field):
|
||||
}
|
||||
|
||||
|
||||
class EventSerializer(I18nAwareModelSerializer):
|
||||
class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
meta_data = MetaDataField(required=False, source='*')
|
||||
item_meta_properties = MetaPropertyField(required=False, source='*')
|
||||
plugins = PluginsField(required=False, source='*')
|
||||
@@ -170,6 +174,13 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
valid_keys = ValidKeysField(source='*', read_only=True)
|
||||
best_availability_state = serializers.IntegerField(allow_null=True, read_only=True)
|
||||
public_url = serializers.SerializerMethodField('get_event_url', read_only=True)
|
||||
limit_sales_channels = serializers.SlugRelatedField(
|
||||
slug_field="identifier",
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
allow_empty=True,
|
||||
many=True,
|
||||
)
|
||||
|
||||
def get_event_url(self, event):
|
||||
return build_absolute_uri(event, 'presale:event.index')
|
||||
@@ -180,7 +191,7 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
'date_to', 'date_admission', 'is_public', 'presale_start',
|
||||
'presale_end', 'location', 'geo_lat', 'geo_lon', 'has_subevents', 'meta_data', 'seating_plan',
|
||||
'plugins', 'seat_category_mapping', 'timezone', 'item_meta_properties', 'valid_keys',
|
||||
'sales_channels', 'best_availability_state', 'public_url')
|
||||
'all_sales_channels', 'limit_sales_channels', 'best_availability_state', 'public_url')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -188,6 +199,7 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
self.fields.pop('valid_keys')
|
||||
if not self.context.get('request') or 'with_availability_for' not in self.context['request'].GET:
|
||||
self.fields.pop('best_availability_state')
|
||||
self.fields['limit_sales_channels'].child_relation.queryset = self.context['organizer'].sales_channels.all()
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
@@ -269,13 +281,17 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
from pretix.base.plugins import get_all_plugins
|
||||
|
||||
plugins_available = {
|
||||
p.module for p in get_all_plugins(self.instance)
|
||||
p.module: p for p in get_all_plugins(self.instance)
|
||||
if not p.name.startswith('.') and getattr(p, 'visible', True)
|
||||
}
|
||||
settings_holder = self.instance if self.instance and self.instance.pk else self.context['organizer']
|
||||
|
||||
for plugin in value.get('plugins'):
|
||||
if plugin not in plugins_available:
|
||||
raise ValidationError(_('Unknown plugin: \'{name}\'.').format(name=plugin))
|
||||
if getattr(plugins_available[plugin], 'restricted', False):
|
||||
if plugin not in settings_holder.settings.allowed_restricted_plugins:
|
||||
raise ValidationError(_('Restricted plugin: \'{name}\'.').format(name=plugin))
|
||||
|
||||
return value
|
||||
|
||||
@@ -472,7 +488,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)
|
||||
@@ -683,10 +700,12 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'locales',
|
||||
'locale',
|
||||
'region',
|
||||
'last_order_modification_date',
|
||||
'allow_modifications',
|
||||
'allow_modifications_after_checkin',
|
||||
'last_order_modification_date',
|
||||
'show_quota_left',
|
||||
'waiting_list_enabled',
|
||||
'waiting_list_auto_disable',
|
||||
'waiting_list_hours',
|
||||
'waiting_list_auto',
|
||||
'waiting_list_names_asked',
|
||||
@@ -733,6 +752,7 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'payment_term_accept_late',
|
||||
'payment_explanation',
|
||||
'payment_pending_hidden',
|
||||
'payment_giftcard__enabled',
|
||||
'mail_days_order_expire_warning',
|
||||
'ticket_download',
|
||||
'ticket_download_date',
|
||||
|
||||
@@ -42,31 +42,43 @@ from django.utils.functional import cached_property, lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from pretix.api.serializers import SalesChannelMigrationMixin
|
||||
from pretix.api.serializers.event import MetaDataField
|
||||
from pretix.api.serializers.fields import UploadedFileField
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.models import (
|
||||
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemVariation,
|
||||
ItemVariationMetaValue, Question, QuestionOption, Quota,
|
||||
ItemVariationMetaValue, Question, QuestionOption, Quota, SalesChannel,
|
||||
)
|
||||
|
||||
|
||||
class InlineItemVariationSerializer(I18nAwareModelSerializer):
|
||||
class InlineItemVariationSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=13,
|
||||
coerce_to_string=True)
|
||||
meta_data = MetaDataField(required=False, source='*')
|
||||
limit_sales_channels = serializers.SlugRelatedField(
|
||||
slug_field="identifier",
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
allow_empty=True,
|
||||
many=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ItemVariation
|
||||
fields = ('id', 'value', 'active', 'description',
|
||||
'position', 'default_price', 'price', 'original_price', 'free_price_suggestion', 'require_approval',
|
||||
'require_membership', 'require_membership_types', 'require_membership_hidden',
|
||||
'checkin_attention', 'checkin_text', 'available_from', 'available_until',
|
||||
'sales_channels', 'hide_without_voucher', 'meta_data')
|
||||
'checkin_attention', 'checkin_text',
|
||||
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
||||
'all_sales_channels', 'limit_sales_channels', 'hide_without_voucher', 'meta_data')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['require_membership_types'].queryset = lazy(lambda: self.context['event'].organizer.membership_types.all(), QuerySet)
|
||||
self.fields['limit_sales_channels'].child_relation.queryset = (
|
||||
self.context['event'].organizer.sales_channels.all() if 'event' in self.context else SalesChannel.objects.none()
|
||||
)
|
||||
|
||||
def validate_meta_data(self, value):
|
||||
for key in value['meta_data'].keys():
|
||||
@@ -75,32 +87,45 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
|
||||
return value
|
||||
|
||||
|
||||
class ItemVariationSerializer(I18nAwareModelSerializer):
|
||||
class ItemVariationSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=13,
|
||||
coerce_to_string=True)
|
||||
meta_data = MetaDataField(required=False, source='*')
|
||||
limit_sales_channels = serializers.SlugRelatedField(
|
||||
slug_field="identifier",
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
allow_empty=True,
|
||||
many=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ItemVariation
|
||||
fields = ('id', 'value', 'active', 'description',
|
||||
'position', 'default_price', 'price', 'original_price', 'free_price_suggestion', 'require_approval',
|
||||
'require_membership', 'require_membership_types', 'require_membership_hidden',
|
||||
'checkin_attention', 'checkin_text', 'available_from', 'available_until',
|
||||
'sales_channels', 'hide_without_voucher', 'meta_data')
|
||||
'checkin_attention', 'checkin_text',
|
||||
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
||||
'all_sales_channels', 'limit_sales_channels', 'hide_without_voucher', 'meta_data')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['require_membership_types'].queryset = self.context['event'].organizer.membership_types.all()
|
||||
self.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||
|
||||
@transaction.atomic
|
||||
def create(self, validated_data):
|
||||
meta_data = validated_data.pop('meta_data', None)
|
||||
require_membership_types = validated_data.pop('require_membership_types', [])
|
||||
limit_sales_channels = validated_data.pop('limit_sales_channels', [])
|
||||
variation = ItemVariation.objects.create(**validated_data)
|
||||
|
||||
if require_membership_types:
|
||||
variation.require_membership_types.add(*require_membership_types)
|
||||
|
||||
if limit_sales_channels:
|
||||
variation.limit_sales_channels.add(*limit_sales_channels)
|
||||
|
||||
# Meta data
|
||||
if meta_data is not None:
|
||||
for key, value in meta_data.items():
|
||||
@@ -221,7 +246,7 @@ class ItemTaxRateField(serializers.Field):
|
||||
return str(Decimal('0.00'))
|
||||
|
||||
|
||||
class ItemSerializer(I18nAwareModelSerializer):
|
||||
class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
addons = InlineItemAddOnSerializer(many=True, required=False)
|
||||
bundles = InlineItemBundleSerializer(many=True, required=False)
|
||||
variations = InlineItemVariationSerializer(many=True, required=False)
|
||||
@@ -230,12 +255,20 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
picture = UploadedFileField(required=False, allow_null=True, allowed_types=(
|
||||
'image/png', 'image/jpeg', 'image/gif'
|
||||
), max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE)
|
||||
limit_sales_channels = serializers.SlugRelatedField(
|
||||
slug_field="identifier",
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
allow_empty=True,
|
||||
many=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Item
|
||||
fields = ('id', 'category', 'name', 'internal_name', 'active', 'sales_channels', 'description',
|
||||
'default_price', 'free_price', 'free_price_suggestion', 'tax_rate', 'tax_rule', 'admission',
|
||||
'personalized', 'position', 'picture', 'available_from', 'available_until',
|
||||
fields = ('id', 'category', 'name', 'internal_name', 'active', 'all_sales_channels', 'limit_sales_channels',
|
||||
'description', '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', 'checkin_text', 'has_variations', 'variations',
|
||||
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
|
||||
@@ -256,6 +289,8 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
if not self.read_only:
|
||||
self.fields['require_membership_types'].queryset = self.context['event'].organizer.membership_types.all()
|
||||
self.fields['grant_membership_type'].queryset = self.context['event'].organizer.membership_types.all()
|
||||
self.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||
self.fields['variations'].child.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
@@ -332,7 +367,10 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
meta_data = validated_data.pop('meta_data', None)
|
||||
picture = validated_data.pop('picture', None)
|
||||
require_membership_types = validated_data.pop('require_membership_types', [])
|
||||
limit_sales_channels = validated_data.pop('limit_sales_channels', [])
|
||||
item = Item.objects.create(**validated_data)
|
||||
if limit_sales_channels:
|
||||
item.limit_sales_channels.add(*limit_sales_channels)
|
||||
if picture:
|
||||
item.picture.save(os.path.basename(picture.name), picture)
|
||||
if require_membership_types:
|
||||
@@ -340,10 +378,13 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
|
||||
for variation_data in variations_data:
|
||||
require_membership_types = variation_data.pop('require_membership_types', [])
|
||||
limit_sales_channels = variation_data.pop('limit_sales_channels', [])
|
||||
var_meta_data = variation_data.pop('meta_data', {})
|
||||
v = ItemVariation.objects.create(item=item, **variation_data)
|
||||
if require_membership_types:
|
||||
v.require_membership_types.add(*require_membership_types)
|
||||
if limit_sales_channels:
|
||||
v.limit_sales_channels.add(*limit_sales_channels)
|
||||
|
||||
if var_meta_data is not None:
|
||||
for key, value in var_meta_data.items():
|
||||
|
||||
@@ -46,13 +46,12 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.item import (
|
||||
InlineItemVariationSerializer, ItemSerializer, QuestionSerializer,
|
||||
)
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedFile, Checkin, Customer, Invoice, InvoiceAddress, InvoiceLine, Item,
|
||||
ItemVariation, Order, OrderPosition, Question, QuestionAnswer,
|
||||
ReusableMedium, Seat, SubEvent, TaxRule, Voucher,
|
||||
ReusableMedium, SalesChannel, Seat, SubEvent, TaxRule, Voucher,
|
||||
)
|
||||
from pretix.base.models.orders import (
|
||||
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
|
||||
@@ -166,7 +165,7 @@ class InlineSeatSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Seat
|
||||
fields = ('id', 'name', 'seat_guid')
|
||||
fields = ('id', 'name', 'seat_guid', 'zone_name', 'row_name', 'row_label', 'seat_label', 'seat_number')
|
||||
|
||||
|
||||
class AnswerSerializer(I18nAwareModelSerializer):
|
||||
@@ -486,11 +485,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):
|
||||
@@ -564,6 +563,8 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
||||
attendee_name = AttendeeNameField(source='*')
|
||||
attendee_name_parts = AttendeeNamePartsField(source='*')
|
||||
order__status = serializers.SlugRelatedField(read_only=True, slug_field='status', source='order')
|
||||
order__valid_if_pending = serializers.SlugRelatedField(read_only=True, slug_field='valid_if_pending', source='order')
|
||||
order__require_approval = serializers.SlugRelatedField(read_only=True, slug_field='require_approval', source='order')
|
||||
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
@@ -571,7 +572,8 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
||||
'company', 'street', 'zipcode', 'city', 'country', 'state',
|
||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'require_attention',
|
||||
'order__status', 'valid_from', 'valid_until', 'blocked')
|
||||
'order__status', 'order__valid_if_pending', 'order__require_approval', 'valid_from', 'valid_until',
|
||||
'blocked')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -583,7 +585,7 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
||||
self.fields['item'] = ItemSerializer(read_only=True, context=self.context)
|
||||
|
||||
if 'variation' in self.context['expand']:
|
||||
self.fields['variation'] = InlineItemVariationSerializer(read_only=True)
|
||||
self.fields['variation'] = InlineItemVariationSerializer(read_only=True, context=self.context)
|
||||
|
||||
if 'answers.question' in self.context['expand']:
|
||||
self.fields['answers'].child.fields['question'] = QuestionSerializer(read_only=True)
|
||||
@@ -711,6 +713,11 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
payment_provider = OrderPaymentTypeField(source='*', read_only=True)
|
||||
url = OrderURLField(source='*', read_only=True)
|
||||
customer = serializers.SlugRelatedField(slug_field='identifier', read_only=True)
|
||||
sales_channel = serializers.SlugRelatedField(
|
||||
slug_field='identifier',
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
@@ -729,6 +736,10 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if "organizer" in self.context:
|
||||
self.fields["sales_channel"].queryset = self.context["organizer"].sales_channels.all()
|
||||
else:
|
||||
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
|
||||
if not self.context['pdf_data']:
|
||||
self.fields['positions'].child.fields.pop('pdf_data', None)
|
||||
|
||||
@@ -1030,12 +1041,18 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
require_approval = serializers.BooleanField(default=False, required=False)
|
||||
simulate = serializers.BooleanField(default=False, required=False)
|
||||
customer = serializers.SlugRelatedField(slug_field='identifier', queryset=Customer.objects.none(), required=False)
|
||||
sales_channel = serializers.SlugRelatedField(
|
||||
slug_field='identifier',
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
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
|
||||
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
@@ -1056,11 +1073,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
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.')
|
||||
return channel
|
||||
|
||||
def validate_code(self, code):
|
||||
if code and Order.objects.filter(event__organizer=self.context['event'].organizer, code=code).exists():
|
||||
raise ValidationError(
|
||||
@@ -1122,20 +1134,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
raise ValidationError(errs)
|
||||
return data
|
||||
|
||||
def validate_testmode(self, testmode):
|
||||
if 'sales_channel' in self.initial_data:
|
||||
try:
|
||||
sales_channel = get_all_sales_channels()[self.initial_data['sales_channel']]
|
||||
|
||||
if testmode and not sales_channel.testmode_supported:
|
||||
raise ValidationError('This sales channel does not provide support for test mode.')
|
||||
except KeyError:
|
||||
# We do not need to raise a ValidationError here, since there is another check to validate the
|
||||
# sales_channel
|
||||
pass
|
||||
|
||||
return testmode
|
||||
|
||||
def create(self, validated_data):
|
||||
fees_data = validated_data.pop('fees') if 'fees' in validated_data else []
|
||||
positions_data = validated_data.pop('positions') if 'positions' in validated_data else []
|
||||
@@ -1144,9 +1142,16 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
payment_date = validated_data.pop('payment_date', now())
|
||||
force = validated_data.pop('force', False)
|
||||
simulate = validated_data.pop('simulate', False)
|
||||
|
||||
if not validated_data.get("sales_channel"):
|
||||
validated_data["sales_channel"] = self.context['event'].organizer.sales_channels.get(identifier="web")
|
||||
|
||||
if validated_data.get("testmode") and not validated_data["sales_channel"].type_instance.testmode_supported:
|
||||
raise ValidationError({"testmode": ["This sales channel does not provide support for test mode."]})
|
||||
|
||||
self._send_mail = validated_data.pop('send_email', False)
|
||||
if self._send_mail is None:
|
||||
self._send_mail = validated_data.get('sales_channel') in self.context['event'].settings.mail_sales_channel_placed_paid
|
||||
self._send_mail = validated_data["sales_channel"].identifier in self.context['event'].settings.mail_sales_channel_placed_paid
|
||||
|
||||
if 'invoice_address' in validated_data:
|
||||
iadata = validated_data.pop('invoice_address')
|
||||
@@ -1306,7 +1311,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
errs[i]['seat'] = ['The specified seat does not exist.']
|
||||
else:
|
||||
seat_usage[seat] += 1
|
||||
if (seat_usage[seat] > 0 and not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web'))) or seat_usage[seat] > 1:
|
||||
sales_channel_id = validated_data['sales_channel'].identifier
|
||||
if (seat_usage[seat] > 0 and not seat.is_available(sales_channel=sales_channel_id)) or seat_usage[seat] > 1:
|
||||
errs[i]['seat'] = [gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)]
|
||||
elif seated:
|
||||
errs[i]['seat'] = ['The specified product requires to choose a seat.']
|
||||
@@ -1315,7 +1321,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()
|
||||
),
|
||||
@@ -1365,6 +1371,7 @@ 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)
|
||||
if not validated_data.get('expires'):
|
||||
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
|
||||
@@ -1439,6 +1446,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
|
||||
@@ -1466,7 +1474,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)
|
||||
@@ -1585,7 +1593,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(
|
||||
@@ -1597,6 +1608,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,
|
||||
|
||||
@@ -38,7 +38,7 @@ from pretix.base.i18n import get_language_without_region
|
||||
from pretix.base.models import (
|
||||
Customer, Device, GiftCard, GiftCardAcceptance, GiftCardTransaction,
|
||||
Membership, MembershipType, OrderPosition, Organizer, ReusableMedium,
|
||||
SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
|
||||
SalesChannel, SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
|
||||
)
|
||||
from pretix.base.models.seating import SeatingPlanLayoutValidator
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
@@ -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:
|
||||
@@ -165,6 +165,36 @@ class FlexibleTicketRelatedField(serializers.PrimaryKeyRelatedField):
|
||||
self.fail('incorrect_type', data_type=type(data).__name__)
|
||||
|
||||
|
||||
class SalesChannelSerializer(I18nAwareModelSerializer):
|
||||
type = serializers.CharField(default="api")
|
||||
|
||||
class Meta:
|
||||
model = SalesChannel
|
||||
fields = ('identifier', 'type', 'label', 'position')
|
||||
|
||||
def validate_type(self, value):
|
||||
if (not self.instance or not self.instance.pk) and value != "api":
|
||||
raise ValidationError(
|
||||
"You can currently only create channels of type 'api' through the API."
|
||||
)
|
||||
if value and self.instance and self.instance.pk and self.instance.type != value:
|
||||
raise ValidationError(
|
||||
"You cannot change the type of a sales channel."
|
||||
)
|
||||
return value
|
||||
|
||||
def validate_identifier(self, value):
|
||||
if (not self.instance or not self.instance.pk) and not value.startswith("api."):
|
||||
raise ValidationError(
|
||||
"Your identifier needs to start with 'api.'."
|
||||
)
|
||||
if value and self.instance and self.instance.pk and self.instance.identifier != value:
|
||||
raise ValidationError(
|
||||
"You cannot change the identifier of a sales channel."
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
class GiftCardSerializer(I18nAwareModelSerializer):
|
||||
value = serializers.DecimalField(max_digits=13, decimal_places=2, min_value=Decimal('0.00'))
|
||||
owner_ticket = FlexibleTicketRelatedField(required=False, allow_null=True, queryset=OrderPosition.all.none())
|
||||
@@ -239,7 +269,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
|
||||
|
||||
@@ -56,6 +56,7 @@ orga_router.register(r'webhooks', webhooks.WebHookViewSet)
|
||||
orga_router.register(r'seatingplans', organizer.SeatingPlanViewSet)
|
||||
orga_router.register(r'giftcards', organizer.GiftCardViewSet)
|
||||
orga_router.register(r'customers', organizer.CustomerViewSet)
|
||||
orga_router.register(r'saleschannels', organizer.SalesChannelViewSet)
|
||||
orga_router.register(r'memberships', organizer.MembershipViewSet)
|
||||
orga_router.register(r'membershiptypes', organizer.MembershipTypeViewSet)
|
||||
orga_router.register(r'reusablemedia', media.ReusableMediaViewSet)
|
||||
|
||||
@@ -211,8 +211,12 @@ class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnly
|
||||
|
||||
if validated_data.get('seat'):
|
||||
# Assumption: Add-ons currently can't have seats, thus we only need to check the main product
|
||||
if validated_data.get('sales_channel'):
|
||||
sales_channel_id = validated_data.get('sales_channel').identifier
|
||||
else:
|
||||
sales_channel_id = "web"
|
||||
if not validated_data['seat'].is_available(
|
||||
sales_channel=validated_data.get('sales_channel', 'web'),
|
||||
sales_channel=sales_channel_id,
|
||||
distance_ignore_cart_id=validated_data['cart_id'],
|
||||
ignore_voucher_id=validated_data['voucher'].pk if validated_data.get('voucher') else None,
|
||||
):
|
||||
|
||||
@@ -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
|
||||
@@ -114,7 +115,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
if 'subevent' in self.request.query_params.getlist('expand'):
|
||||
qs = qs.prefetch_related(
|
||||
'subevent', 'subevent__event', 'subevent__subeventitem_set', 'subevent__subeventitemvariation_set',
|
||||
'subevent__seat_category_mappings', 'subevent__meta_values'
|
||||
'subevent__seat_category_mappings', 'subevent__meta_values', 'auto_checkin_sales_channels'
|
||||
)
|
||||
return qs
|
||||
|
||||
@@ -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(
|
||||
@@ -403,7 +406,7 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
|
||||
'item__variations').select_related('item__tax_rule')
|
||||
|
||||
if expand and 'variation' in expand:
|
||||
qs = qs.prefetch_related('variation')
|
||||
qs = qs.prefetch_related('variation', 'variation__meta_values')
|
||||
|
||||
return qs
|
||||
|
||||
@@ -584,6 +587,32 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
'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())
|
||||
|
||||
@@ -60,7 +60,9 @@ class DiscountViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
write_permission = 'can_change_items'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.discounts.all()
|
||||
return self.request.event.discounts.prefetch_related(
|
||||
'limit_sales_channels',
|
||||
)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
|
||||
@@ -41,6 +41,7 @@ from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import serializers, views, viewsets
|
||||
from rest_framework.exceptions import PermissionDenied, ValidationError
|
||||
from rest_framework.generics import get_object_or_404
|
||||
from rest_framework.response import Response
|
||||
|
||||
from pretix.api.auth.permission import EventCRUDPermission
|
||||
@@ -57,10 +58,8 @@ from pretix.base.models import (
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.services.quotas import QuotaAvailability
|
||||
from pretix.base.settings import SETTINGS_AFFECTING_CSS
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
from pretix.helpers.i18n import i18ncomp
|
||||
from pretix.presale.style import regenerate_css
|
||||
from pretix.presale.views.organizer import filter_qs_by_attr
|
||||
|
||||
with scopes_disabled():
|
||||
@@ -115,7 +114,10 @@ with scopes_disabled():
|
||||
return queryset.exclude(expr)
|
||||
|
||||
def sales_channel_qs(self, queryset, name, value):
|
||||
return queryset.filter(sales_channels__contains=value)
|
||||
return queryset.filter(
|
||||
Q(all_sales_channels=True) |
|
||||
Q(limit_sales_channels__identifier=value)
|
||||
)
|
||||
|
||||
def search_qs(self, queryset, name, value):
|
||||
return queryset.filter(
|
||||
@@ -137,6 +139,12 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
ordering_fields = ('date_from', 'slug')
|
||||
filterset_class = EventFilter
|
||||
|
||||
def get_serializer_context(self):
|
||||
return {
|
||||
**super().get_serializer_context(),
|
||||
"organizer": self.request.organizer,
|
||||
}
|
||||
|
||||
def get_copy_from_queryset(self):
|
||||
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
||||
return self.request.auth.get_events_with_any_permission()
|
||||
@@ -155,13 +163,20 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
qs = filter_qs_by_attr(qs, self.request)
|
||||
|
||||
if 'with_availability_for' in self.request.GET:
|
||||
qs = Event.annotated(qs, channel=self.request.GET.get('with_availability_for'))
|
||||
qs = Event.annotated(
|
||||
qs,
|
||||
channel=get_object_or_404(
|
||||
self.request.organizer.sales_channels,
|
||||
identifier=self.request.GET.get('with_availability_for')
|
||||
)
|
||||
)
|
||||
|
||||
return qs.prefetch_related(
|
||||
'organizer',
|
||||
'meta_values',
|
||||
'meta_values__property',
|
||||
'item_meta_properties',
|
||||
'limit_sales_channels',
|
||||
Prefetch(
|
||||
'seat_category_mappings',
|
||||
to_attr='_seat_category_mappings',
|
||||
@@ -190,7 +205,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 +216,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(
|
||||
@@ -262,8 +285,6 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
new_event.is_public = serializer.validated_data['is_public']
|
||||
if 'testmode' in serializer.validated_data:
|
||||
new_event.testmode = serializer.validated_data['testmode']
|
||||
if 'sales_channels' in serializer.validated_data:
|
||||
new_event.sales_channels = serializer.validated_data['sales_channels']
|
||||
if 'has_subevents' in serializer.validated_data:
|
||||
new_event.has_subevents = serializer.validated_data['has_subevents']
|
||||
if 'date_admission' in serializer.validated_data:
|
||||
@@ -271,6 +292,10 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
new_event.save()
|
||||
if 'timezone' in serializer.validated_data:
|
||||
new_event.settings.timezone = serializer.validated_data['timezone']
|
||||
|
||||
if 'all_sales_channels' in serializer.validated_data and 'sales_channels' in serializer.validated_data:
|
||||
new_event.all_sales_channels = serializer.validated_data['all_sales_channels']
|
||||
new_event.limit_sales_channels.set(serializer.validated_data['limit_sales_channels'])
|
||||
else:
|
||||
serializer.instance.set_defaults()
|
||||
|
||||
@@ -291,7 +316,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),
|
||||
@@ -373,7 +398,10 @@ with scopes_disabled():
|
||||
return queryset.exclude(expr)
|
||||
|
||||
def sales_channel_qs(self, queryset, name, value):
|
||||
return queryset.filter(event__sales_channels__contains=value)
|
||||
return queryset.filter(
|
||||
Q(event__all_sales_channels=True) |
|
||||
Q(event__limit_sales_channels__identifier=value)
|
||||
)
|
||||
|
||||
def search_qs(self, queryset, name, value):
|
||||
return queryset.filter(
|
||||
@@ -421,7 +449,13 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
qs = filter_qs_by_attr(qs, self.request)
|
||||
|
||||
if 'with_availability_for' in self.request.GET:
|
||||
qs = SubEvent.annotated(qs, channel=self.request.GET.get('with_availability_for'))
|
||||
qs = SubEvent.annotated(
|
||||
qs,
|
||||
channel=get_object_or_404(
|
||||
self.request.organizer.sales_channels,
|
||||
identifier=self.request.GET.get('with_availability_for')
|
||||
)
|
||||
)
|
||||
|
||||
return qs.prefetch_related(
|
||||
'event',
|
||||
@@ -622,13 +656,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 any(p in s.changed_data for p in SETTINGS_AFFECTING_CSS):
|
||||
regenerate_css.apply_async(args=(request.event.pk,))
|
||||
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()
|
||||
}
|
||||
)
|
||||
s = EventSettingsSerializer(
|
||||
instance=request.event.settings, event=request.event, context={
|
||||
'request': request
|
||||
|
||||
@@ -56,10 +56,17 @@ from pretix.base.models import (
|
||||
)
|
||||
from pretix.base.services.quotas import QuotaAvailability
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
from pretix.helpers.i18n import i18ncomp
|
||||
|
||||
with scopes_disabled():
|
||||
class ItemFilter(FilterSet):
|
||||
tax_rate = django_filters.CharFilter(method='tax_rate_qs')
|
||||
search = django_filters.CharFilter(method='search_qs')
|
||||
|
||||
def search_qs(self, queryset, name, value):
|
||||
return queryset.filter(
|
||||
Q(internal_name__icontains=value) | Q(name__icontains=i18ncomp(value))
|
||||
)
|
||||
|
||||
def tax_rate_qs(self, queryset, name, value):
|
||||
if value in ("0", "None", "0.00"):
|
||||
@@ -71,6 +78,18 @@ with scopes_disabled():
|
||||
model = Item
|
||||
fields = ['active', 'category', 'admission', 'tax_rate', 'free_price']
|
||||
|
||||
class ItemVariationFilter(FilterSet):
|
||||
search = django_filters.CharFilter(method='search_qs')
|
||||
|
||||
def search_qs(self, queryset, name, value):
|
||||
return queryset.filter(
|
||||
Q(value__icontains=i18ncomp(value))
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ItemVariation
|
||||
fields = ['active']
|
||||
|
||||
|
||||
class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
serializer_class = ItemSerializer
|
||||
@@ -87,6 +106,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
'variations', 'addons', 'bundles', 'meta_values', 'meta_values__property',
|
||||
'variations__meta_values', 'variations__meta_values__property',
|
||||
'require_membership_types', 'variations__require_membership_types',
|
||||
'limit_sales_channels', 'variations__limit_sales_channels',
|
||||
).all()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
@@ -139,6 +159,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = ItemVariationSerializer
|
||||
queryset = ItemVariation.objects.none()
|
||||
filter_backends = (DjangoFilterBackend, TotalOrderingFilter,)
|
||||
filterset_class = ItemVariationFilter
|
||||
ordering_fields = ('id', 'position')
|
||||
ordering = ('id',)
|
||||
permission = None
|
||||
@@ -152,7 +173,8 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
||||
return self.item.variations.all().prefetch_related(
|
||||
'meta_values',
|
||||
'meta_values__property',
|
||||
'require_membership_types'
|
||||
'require_membership_types',
|
||||
'limit_sales_channels',
|
||||
)
|
||||
|
||||
def get_serializer_context(self):
|
||||
|
||||
@@ -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):
|
||||
@@ -104,6 +108,7 @@ with scopes_disabled():
|
||||
status = django_filters.CharFilter(field_name='status', lookup_expr='iexact')
|
||||
modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte')
|
||||
created_since = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='gte')
|
||||
created_before = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='lt')
|
||||
subevent_after = django_filters.IsoDateTimeFilter(method='subevent_after_qs')
|
||||
subevent_before = django_filters.IsoDateTimeFilter(method='subevent_before_qs')
|
||||
search = django_filters.CharFilter(method='search_qs')
|
||||
@@ -111,6 +116,8 @@ with scopes_disabled():
|
||||
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')
|
||||
sales_channel = django_filters.CharFilter(field_name='sales_channel__identifier')
|
||||
payment_provider = django_filters.CharFilter(method='provider_qs')
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
@@ -134,6 +141,11 @@ with scopes_disabled():
|
||||
)
|
||||
return qs
|
||||
|
||||
def provider_qs(self, qs, name, value):
|
||||
return qs.filter(Exists(
|
||||
OrderPayment.objects.filter(order=OuterRef('pk'), provider=value)
|
||||
))
|
||||
|
||||
def subevent_before_qs(self, qs, name, value):
|
||||
if getattr(self.request, 'event', None):
|
||||
subevents = self.request.event.subevents
|
||||
@@ -225,7 +237,7 @@ class OrderViewSetMixin:
|
||||
if 'customer' not in self.request.GET.getlist('exclude'):
|
||||
qs = qs.select_related('customer')
|
||||
|
||||
qs = qs.prefetch_related(self._positions_prefetch(self.request))
|
||||
qs = qs.select_related('sales_channel').prefetch_related(self._positions_prefetch(self.request))
|
||||
return qs
|
||||
|
||||
def _positions_prefetch(self, request):
|
||||
@@ -312,6 +324,11 @@ class OrganizerOrderViewSet(OrderViewSetMixin, viewsets.ReadOnlyModelViewSet):
|
||||
else:
|
||||
raise PermissionDenied()
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['organizer'] = self.request.organizer
|
||||
return ctx
|
||||
|
||||
|
||||
class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
||||
permission = 'can_view_orders'
|
||||
@@ -572,8 +589,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()
|
||||
@@ -900,7 +919,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:
|
||||
@@ -1898,6 +1921,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:
|
||||
@@ -1905,9 +1929,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,10 +24,11 @@ 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 import OuterRef, Q, 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.utils.timezone import now
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import mixins, serializers, status, views, viewsets
|
||||
@@ -43,18 +44,16 @@ from pretix.api.serializers.organizer import (
|
||||
CustomerCreateSerializer, CustomerSerializer, DeviceSerializer,
|
||||
GiftCardSerializer, GiftCardTransactionSerializer, MembershipSerializer,
|
||||
MembershipTypeSerializer, OrganizerSerializer, OrganizerSettingsSerializer,
|
||||
SeatingPlanSerializer, TeamAPITokenSerializer, TeamInviteSerializer,
|
||||
TeamMemberSerializer, TeamSerializer,
|
||||
SalesChannelSerializer, SeatingPlanSerializer, TeamAPITokenSerializer,
|
||||
TeamInviteSerializer, TeamMemberSerializer, TeamSerializer,
|
||||
)
|
||||
from pretix.base.models import (
|
||||
Customer, Device, GiftCard, GiftCardTransaction, Membership,
|
||||
MembershipType, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite,
|
||||
User,
|
||||
MembershipType, Organizer, SalesChannel, SeatingPlan, Team, TeamAPIToken,
|
||||
TeamInvite, User,
|
||||
)
|
||||
from pretix.base.settings import SETTINGS_AFFECTING_CSS
|
||||
from pretix.helpers import OF_SELF
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
from pretix.presale.style import regenerate_organizer_css
|
||||
|
||||
|
||||
class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
@@ -138,11 +137,19 @@ class SeatingPlanViewSet(viewsets.ModelViewSet):
|
||||
with scopes_disabled():
|
||||
class GiftCardFilter(FilterSet):
|
||||
secret = django_filters.CharFilter(field_name='secret', lookup_expr='iexact')
|
||||
expired = django_filters.BooleanFilter(method='expired_qs')
|
||||
value = django_filters.NumberFilter(field_name='cached_value')
|
||||
|
||||
class Meta:
|
||||
model = GiftCard
|
||||
fields = ['secret', 'testmode']
|
||||
|
||||
def expired_qs(self, qs, name, value):
|
||||
if value:
|
||||
return qs.filter(expires__isnull=False, expires__lt=now())
|
||||
else:
|
||||
return qs.filter(Q(expires__isnull=True) | Q(expires__gte=now()))
|
||||
|
||||
|
||||
class GiftCardViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = GiftCardSerializer
|
||||
@@ -504,8 +511,6 @@ class OrganizerSettingsView(views.APIView):
|
||||
k: v for k, v in s.validated_data.items()
|
||||
}
|
||||
)
|
||||
if any(p in s.changed_data for p in SETTINGS_AFFECTING_CSS):
|
||||
regenerate_organizer_css.apply_async(args=(request.organizer.pk,))
|
||||
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer, context={
|
||||
'request': request
|
||||
})
|
||||
@@ -679,3 +684,68 @@ class MembershipViewSet(viewsets.ModelViewSet):
|
||||
data=self.request.data,
|
||||
)
|
||||
return inst
|
||||
|
||||
|
||||
with scopes_disabled():
|
||||
class SalesChannelFilter(FilterSet):
|
||||
class Meta:
|
||||
model = SalesChannel
|
||||
fields = ['type', 'identifier']
|
||||
|
||||
|
||||
class SalesChannelViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = SalesChannelSerializer
|
||||
queryset = SalesChannel.objects.none()
|
||||
permission = 'can_change_organizer_settings'
|
||||
write_permission = 'can_change_organizer_settings'
|
||||
filter_backends = (DjangoFilterBackend,)
|
||||
filterset_class = SalesChannelFilter
|
||||
lookup_field = 'identifier'
|
||||
lookup_url_kwarg = 'identifier'
|
||||
lookup_value_regex = r"[a-zA-Z0-9.\-_]+"
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.organizer.sales_channels.all()
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['organizer'] = self.request.organizer
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
inst = serializer.save(
|
||||
organizer=self.request.organizer,
|
||||
type="api"
|
||||
)
|
||||
inst.log_action(
|
||||
'pretix.saleschannel.created',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=merge_dicts(self.request.data, {'id': inst.pk})
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
inst = serializer.save(
|
||||
type=serializer.instance.type,
|
||||
identifier=serializer.instance.identifier,
|
||||
)
|
||||
inst.log_action(
|
||||
'pretix.sales_channel.changed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data,
|
||||
)
|
||||
return inst
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
if not instance.allow_delete():
|
||||
raise PermissionDenied("Can only be deleted if unused.")
|
||||
instance.log_action(
|
||||
'pretix.saleschannel.deleted',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={'id': instance.pk}
|
||||
)
|
||||
instance.delete()
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -20,56 +20,83 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import logging
|
||||
import warnings
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.signals import register_sales_channels
|
||||
from pretix.base.signals import (
|
||||
register_sales_channel_types, register_sales_channels,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_ALL_CHANNELS = None
|
||||
_ALL_CHANNEL_TYPES = None
|
||||
|
||||
|
||||
class SalesChannel:
|
||||
class SalesChannelType:
|
||||
def __repr__(self):
|
||||
return '<SalesChannel: {}>'.format(self.identifier)
|
||||
return '<SalesChannelType: {}>'.format(self.identifier)
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
"""
|
||||
The internal identifier of this sales channel.
|
||||
The internal identifier of this sales channel type.
|
||||
"""
|
||||
raise NotImplementedError() # NOQA
|
||||
|
||||
@property
|
||||
def verbose_name(self) -> str:
|
||||
"""
|
||||
A human-readable name of this sales channel.
|
||||
A human-readable name of this sales channel type.
|
||||
"""
|
||||
raise NotImplementedError() # NOQA
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
"""
|
||||
A human-readable description of this sales channel type.
|
||||
"""
|
||||
return ""
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
"""
|
||||
The name of a Font Awesome icon to represent this channel
|
||||
This can be:
|
||||
|
||||
- The name of a Font Awesome icon to represent this channel type.
|
||||
- The name of a SVG icon file that is resolvable through the static file system. We recommend to design for a size of 18x14 pixels.
|
||||
"""
|
||||
return "circle"
|
||||
|
||||
@property
|
||||
def default_created(self) -> bool:
|
||||
"""
|
||||
Indication, if a sales channel of this type should automatically be created for every organizer
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def multiple_allowed(self) -> bool:
|
||||
"""
|
||||
Indication, if multiple sales channels of this type may exist in the same organizer
|
||||
"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def testmode_supported(self) -> bool:
|
||||
"""
|
||||
Indication, if a saleschannels supports test mode orders
|
||||
Indication, if a sales channel of this type supports test mode orders
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def payment_restrictions_supported(self) -> bool:
|
||||
"""
|
||||
If this property is ``True``, organizers can restrict the usage of payment providers to this sales channel.
|
||||
If this property is ``True``, organizers can restrict the usage of payment providers to this sales channel type.
|
||||
|
||||
Example: pretixPOS provides its own sales channel, ignores the configured payment providers completely and
|
||||
handles payments locally. Therefor, this property should be set to ``False`` for the pretixPOS sales channel as
|
||||
Example: pretixPOS provides its own sales channel type, ignores the configured payment providers completely and
|
||||
handles payments locally. Therefore, this property should be set to ``False`` for the pretixPOS sales channel as
|
||||
the event organizer cannot restrict the usage of any payment provider through the backend.
|
||||
"""
|
||||
return True
|
||||
@@ -77,8 +104,8 @@ class SalesChannel:
|
||||
@property
|
||||
def unlimited_items_per_order(self) -> bool:
|
||||
"""
|
||||
If this property is ``True``, purchases made using this sales channel are not limited to the maximum amount of
|
||||
items defined in the event settings.
|
||||
If this property is ``True``, purchases made using sales channels of this type are not limited to the maximum
|
||||
amount of items defined in the event settings.
|
||||
"""
|
||||
return False
|
||||
|
||||
@@ -96,34 +123,67 @@ class SalesChannel:
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def required_event_plugin(self) -> str:
|
||||
"""
|
||||
Name of an event plugin that is required for this sales channel to be useful. Defaults to ``None``.
|
||||
"""
|
||||
return
|
||||
|
||||
def get_all_sales_channels():
|
||||
global _ALL_CHANNELS
|
||||
|
||||
if _ALL_CHANNELS:
|
||||
return _ALL_CHANNELS
|
||||
def get_all_sales_channel_types():
|
||||
from pretix.base.signals import register_sales_channel_types
|
||||
global _ALL_CHANNEL_TYPES
|
||||
|
||||
if _ALL_CHANNEL_TYPES:
|
||||
return _ALL_CHANNEL_TYPES
|
||||
|
||||
channels = []
|
||||
for recv, ret in register_sales_channels.send(None):
|
||||
for recv, ret in register_sales_channel_types.send(None):
|
||||
if isinstance(ret, (list, tuple)):
|
||||
channels += ret
|
||||
else:
|
||||
channels.append(ret)
|
||||
for recv, ret in register_sales_channels.send(None): # todo: remove me
|
||||
if isinstance(ret, (list, tuple)):
|
||||
channels += ret
|
||||
else:
|
||||
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
|
||||
_ALL_CHANNEL_TYPES = OrderedDict([(c.identifier, c) for c in channels])
|
||||
if 'web' in _ALL_CHANNEL_TYPES:
|
||||
_ALL_CHANNEL_TYPES.move_to_end('web', last=False)
|
||||
return _ALL_CHANNEL_TYPES
|
||||
|
||||
|
||||
class WebshopSalesChannel(SalesChannel):
|
||||
def get_all_sales_channels():
|
||||
# TODO: remove me
|
||||
warnings.warn('Using get_all_sales_channels() is no longer appropriate, use get_al_sales_channel_types() instead.',
|
||||
DeprecationWarning, stacklevel=2)
|
||||
return get_all_sales_channel_types()
|
||||
|
||||
|
||||
class WebshopSalesChannelType(SalesChannelType):
|
||||
identifier = "web"
|
||||
verbose_name = _('Online shop')
|
||||
icon = "globe"
|
||||
|
||||
|
||||
@receiver(register_sales_channels, dispatch_uid="base_register_default_sales_channels")
|
||||
class ApiSalesChannelType(SalesChannelType):
|
||||
identifier = "api"
|
||||
verbose_name = _('API')
|
||||
description = _('API sales channels come with no built-in functionality, but may be used for custom integrations.')
|
||||
icon = "exchange"
|
||||
default_created = False
|
||||
multiple_allowed = True
|
||||
|
||||
|
||||
SalesChannel = SalesChannelType # TODO: remove me
|
||||
|
||||
|
||||
@receiver(register_sales_channel_types, dispatch_uid="base_register_default_sales_channel_types")
|
||||
def base_sales_channels(sender, **kwargs):
|
||||
return (
|
||||
WebshopSalesChannel(),
|
||||
WebshopSalesChannelType(),
|
||||
ApiSalesChannelType(),
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
@@ -192,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
|
||||
@@ -217,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()
|
||||
|
||||
@@ -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 '
|
||||
|
||||
@@ -27,7 +27,6 @@ from openpyxl.styles import Alignment
|
||||
from openpyxl.utils import get_column_letter
|
||||
|
||||
from ...helpers.safe_openpyxl import SafeCell
|
||||
from ..channels import get_all_sales_channels
|
||||
from ..exporter import ListExporter
|
||||
from ..models import ItemMetaValue, ItemVariation, ItemVariationMetaValue
|
||||
from ..signals import register_data_exporters
|
||||
@@ -53,7 +52,7 @@ class ItemDataExporter(ListExporter):
|
||||
|
||||
def iterate_list(self, form_data):
|
||||
locales = self.event.settings.locales
|
||||
scs = get_all_sales_channels()
|
||||
scs = self.organizer.sales_channels.all()
|
||||
header = [
|
||||
_("Product ID"),
|
||||
_("Variation ID"),
|
||||
@@ -141,9 +140,15 @@ class ItemDataExporter(ListExporter):
|
||||
row.append(i.name.localize(l))
|
||||
for l in locales:
|
||||
row.append(v.value.localize(l))
|
||||
|
||||
sales_channels = list(scs)
|
||||
if not i.all_sales_channels:
|
||||
sales_channels = [s for s in sales_channels if s.identifier in (p.identifier for p in i.limit_sales_channels.all())]
|
||||
if not v.all_sales_channels:
|
||||
sales_channels = [s for s in sales_channels if s.identifier in (p.identifier for p in v.limit_sales_channels.all())]
|
||||
row += [
|
||||
_("Yes") if i.active and v.active else "",
|
||||
", ".join([str(sn.verbose_name) for s, sn in scs.items() if s in i.sales_channels and s in v.sales_channels]),
|
||||
", ".join([str(sn.label) for sn in sales_channels]),
|
||||
v.default_price or i.default_price,
|
||||
_("Yes") if i.free_price else "",
|
||||
str(i.tax_rule) if i.tax_rule else "",
|
||||
@@ -186,9 +191,12 @@ class ItemDataExporter(ListExporter):
|
||||
row.append(i.name.localize(l))
|
||||
for l in locales:
|
||||
row.append("")
|
||||
sales_channels = list(scs)
|
||||
if not i.all_sales_channels:
|
||||
sales_channels = [s for s in sales_channels if s.identifier in (p.identifier for p in i.limit_sales_channels.all())]
|
||||
row += [
|
||||
_("Yes") if i.active else "",
|
||||
", ".join([str(sn.verbose_name) for s, sn in scs.items() if s in i.sales_channels]),
|
||||
", ".join([str(sn.label) for sn in sales_channels]),
|
||||
i.default_price,
|
||||
_("Yes") if i.free_price else "",
|
||||
str(i.tax_rule) if i.tax_rule else "",
|
||||
|
||||
@@ -54,6 +54,7 @@ class JSONExporter(BaseExporter):
|
||||
'import in third-party systems.')
|
||||
|
||||
def render(self, form_data):
|
||||
all_sales_channels = self.event.organizer.sales_channels.all()
|
||||
jo = {
|
||||
'event': {
|
||||
'name': str(self.event.name),
|
||||
@@ -85,7 +86,7 @@ class JSONExporter(BaseExporter):
|
||||
'admission': item.admission,
|
||||
'personalized': item.personalized,
|
||||
'active': item.active,
|
||||
'sales_channels': item.sales_channels,
|
||||
'sales_channels': [c.identifier for c in (all_sales_channels if item.all_sales_channels else item.limit_sales_channels.all())],
|
||||
'description': str(item.description),
|
||||
'available_from': item.available_from,
|
||||
'available_until': item.available_until,
|
||||
@@ -114,7 +115,9 @@ class JSONExporter(BaseExporter):
|
||||
'checkin_text': variation.checkin_text,
|
||||
'require_approval': variation.require_approval,
|
||||
'require_membership': variation.require_membership,
|
||||
'sales_channels': variation.sales_channels,
|
||||
'sales_channels': [
|
||||
c.identifier for c in (all_sales_channels if variation.all_sales_channels else variation.limit_sales_channels.all())
|
||||
],
|
||||
'available_from': variation.available_from,
|
||||
'available_until': variation.available_until,
|
||||
'hide_without_voucher': variation.hide_without_voucher,
|
||||
@@ -122,6 +125,7 @@ class JSONExporter(BaseExporter):
|
||||
} for variation in item.variations.all()
|
||||
]
|
||||
} for item in self.event.items.select_related('tax_rule').prefetch_related(
|
||||
'limit_sales_channels',
|
||||
Prefetch(
|
||||
'meta_values',
|
||||
ItemMetaValue.objects.select_related('property'),
|
||||
@@ -130,6 +134,7 @@ class JSONExporter(BaseExporter):
|
||||
Prefetch(
|
||||
'variations',
|
||||
queryset=ItemVariation.objects.prefetch_related(
|
||||
'limit_sales_channels',
|
||||
Prefetch(
|
||||
'meta_values',
|
||||
ItemVariationMetaValue.objects.select_related('property'),
|
||||
@@ -167,7 +172,7 @@ class JSONExporter(BaseExporter):
|
||||
'require_approval': order.require_approval,
|
||||
'checkin_attention': order.checkin_attention,
|
||||
'checkin_text': order.checkin_text,
|
||||
'sales_channel': order.sales_channel,
|
||||
'sales_channel': order.sales_channel.identifier,
|
||||
'expires': order.expires,
|
||||
'datetime': order.datetime,
|
||||
'fees': [
|
||||
|
||||
@@ -32,11 +32,12 @@
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
from collections import OrderedDict
|
||||
from collections import OrderedDict, defaultdict
|
||||
from decimal import Decimal
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.db.models import (
|
||||
Case, CharField, Count, DateTimeField, F, IntegerField, Max, Min, OuterRef,
|
||||
Q, Subquery, Sum, When,
|
||||
@@ -54,7 +55,7 @@ from openpyxl.comments import Comment
|
||||
from openpyxl.styles import Font, PatternFill
|
||||
|
||||
from pretix.base.models import (
|
||||
GiftCard, GiftCardTransaction, Invoice, InvoiceAddress, Order,
|
||||
Checkin, GiftCard, GiftCardTransaction, Invoice, InvoiceAddress, Order,
|
||||
OrderPosition, Question,
|
||||
)
|
||||
from pretix.base.models.orders import (
|
||||
@@ -209,7 +210,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 +251,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:
|
||||
@@ -331,6 +336,7 @@ 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_extended_status_display(),
|
||||
@@ -406,7 +412,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,
|
||||
@@ -425,9 +431,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'),
|
||||
@@ -464,6 +475,7 @@ 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_extended_status_display(),
|
||||
order.email,
|
||||
@@ -506,7 +518,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,
|
||||
@@ -516,11 +540,24 @@ 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()),
|
||||
checked_in_lists=Subquery(
|
||||
Checkin.objects.filter(
|
||||
successful=True,
|
||||
type=Checkin.TYPE_ENTRY,
|
||||
position=OuterRef("pk"),
|
||||
).order_by().values("position").annotate(
|
||||
c=GroupConcat(
|
||||
"list__name",
|
||||
# These appear not to work properly on SQLite. Well, we don't support SQLite outside testing
|
||||
# anyways.
|
||||
ordered='sqlite' not in settings.DATABASES['default']['ENGINE'],
|
||||
distinct='sqlite' not in settings.DATABASES['default']['ENGINE'],
|
||||
delimiter=", "
|
||||
)
|
||||
).values("c")
|
||||
),
|
||||
).select_related(
|
||||
'order', 'order__invoice_address', 'order__customer', 'item', 'variation',
|
||||
'voucher', 'tax_rule'
|
||||
@@ -528,15 +565,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'),
|
||||
@@ -588,10 +622,9 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
]
|
||||
|
||||
questions = list(Question.objects.filter(event__in=self.events))
|
||||
options = {}
|
||||
options = defaultdict(list)
|
||||
for q in questions:
|
||||
if q.type == Question.TYPE_CHOICE_MULTIPLE:
|
||||
options[q.pk] = []
|
||||
if form_data['group_multiple_choice']:
|
||||
for o in q.options.all():
|
||||
options[q.pk].append(o)
|
||||
@@ -601,6 +634,9 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
headers.append(str(q.question) + ' – ' + str(o.answer))
|
||||
options[q.pk].append(o)
|
||||
else:
|
||||
if q.type == Question.TYPE_CHOICE:
|
||||
for o in q.options.all():
|
||||
options[q.pk].append(o)
|
||||
headers.append(str(q.question))
|
||||
headers += [
|
||||
_('Company'),
|
||||
@@ -619,6 +655,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
_('Sales channel'), _('Order locale'),
|
||||
_('E-mail address verified'),
|
||||
_('External customer ID'),
|
||||
_('Check-in lists'),
|
||||
_('Payment providers'),
|
||||
]
|
||||
|
||||
@@ -638,6 +675,7 @@ 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_extended_status_display(),
|
||||
@@ -709,7 +747,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
for a in op.answers.all():
|
||||
# We do not want to localize Date, Time and Datetime question answers, as those can lead
|
||||
# to difficulties parsing the data (for example 2019-02-01 may become Février, 2019 01 in French).
|
||||
if a.question.type == Question.TYPE_CHOICE_MULTIPLE:
|
||||
if a.question.type in (Question.TYPE_CHOICE_MULTIPLE, Question.TYPE_CHOICE):
|
||||
acache[a.question_id] = set(o.pk for o in a.options.all())
|
||||
elif a.question.type in Question.UNLOCALIZED_TYPES:
|
||||
acache[a.question_id] = a.answer
|
||||
@@ -722,6 +760,10 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
else:
|
||||
for o in options[q.pk]:
|
||||
row.append(_('Yes') if o.pk in acache.get(q.pk, set()) else _('No'))
|
||||
elif q.type == Question.TYPE_CHOICE:
|
||||
# Join is only necessary if the question type was modified but also keeps the code simpler here
|
||||
# as we'd otherwise need some [0] and existance checks
|
||||
row.append(", ".join(str(o.answer) for o in options[q.pk] if o.pk in acache.get(q.pk, set())))
|
||||
else:
|
||||
row.append(acache.get(q.pk, ''))
|
||||
|
||||
@@ -752,6 +794,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
_('Yes') if order.email_known_to_work else _('No'),
|
||||
str(order.customer.external_identifier) if order.customer and order.customer.external_identifier else '',
|
||||
]
|
||||
row.append(op.checked_in_lists or "")
|
||||
row.append(', '.join([
|
||||
str(self.providers.get(p, p)) for p in sorted(set((op.payment_providers or '').split(',')))
|
||||
if p and p != 'free'
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -606,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 = {}
|
||||
@@ -1023,7 +1035,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
self.all_optional = kwargs.pop('all_optional', False)
|
||||
|
||||
kwargs.setdefault('initial', {})
|
||||
if not kwargs.get('instance') or not kwargs['instance'].country:
|
||||
if (not kwargs.get('instance') or not kwargs['instance'].country) and not kwargs["initial"].get("country"):
|
||||
kwargs['initial']['country'] = guess_country_from_request(self.request, self.event)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -1159,7 +1171,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
self.instance.vat_id_validated = False
|
||||
messages.warning(self.request, e.message)
|
||||
else:
|
||||
raise ValidationError(e.message)
|
||||
raise ValidationError({"vat_id": e.message})
|
||||
except VATIDTemporaryError as e:
|
||||
self.instance.vat_id_validated = False
|
||||
if self.request and self.vat_warning:
|
||||
|
||||
@@ -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
|
||||
@@ -32,13 +32,13 @@
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import re
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import BaseValidator
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.helpers.format import format_map
|
||||
|
||||
|
||||
class PlaceholderValidator(BaseValidator):
|
||||
"""
|
||||
@@ -47,6 +47,12 @@ class PlaceholderValidator(BaseValidator):
|
||||
which are not presented in taken list.
|
||||
"""
|
||||
|
||||
error_message = _(
|
||||
'There is an error with your placeholder syntax. Please check that the opening "{" and closing "}" curly '
|
||||
'brackets on your placeholders match up. '
|
||||
'Please note: to use literal "{" or "}", you need to double them as "{{" and "}}".'
|
||||
)
|
||||
|
||||
def __init__(self, limit_value):
|
||||
super().__init__(limit_value)
|
||||
self.limit_value = limit_value
|
||||
@@ -57,22 +63,15 @@ class PlaceholderValidator(BaseValidator):
|
||||
self.__call__(v)
|
||||
return
|
||||
|
||||
if value.count('{') != value.count('}'):
|
||||
try:
|
||||
format_map(value, {key.strip('{}'): "" for key in self.limit_value}, raise_on_missing=True)
|
||||
except ValueError:
|
||||
raise ValidationError(self.error_message, code='invalid_placeholder_syntax')
|
||||
except KeyError as e:
|
||||
raise ValidationError(
|
||||
_('Invalid placeholder syntax: You used a different number of "{" than of "}".'),
|
||||
code='invalid_placeholder_syntax',
|
||||
)
|
||||
|
||||
data_placeholders = list(re.findall(r'({[^}]*})', value, re.X))
|
||||
invalid_placeholders = []
|
||||
for placeholder in data_placeholders:
|
||||
if placeholder not in self.limit_value:
|
||||
invalid_placeholders.append(placeholder)
|
||||
if invalid_placeholders:
|
||||
raise ValidationError(
|
||||
_('Invalid placeholder(s): %(value)s'),
|
||||
_('Invalid placeholder: {%(value)s}'),
|
||||
code='invalid_placeholders',
|
||||
params={'value': ", ".join(invalid_placeholders,)})
|
||||
params={'value': e.args[0]})
|
||||
|
||||
def clean(self, x):
|
||||
return x
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from collections import OrderedDict
|
||||
from urllib.parse import urlsplit
|
||||
from urllib.parse import urlparse, urlsplit
|
||||
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||
|
||||
from django.conf import settings
|
||||
@@ -40,6 +40,7 @@ from pretix.base.settings import global_settings_object
|
||||
from pretix.multidomain.urlreverse import (
|
||||
get_event_domain, get_organizer_domain,
|
||||
)
|
||||
from pretix.presale.style import get_fonts
|
||||
|
||||
_supported = None
|
||||
|
||||
@@ -240,6 +241,14 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
)
|
||||
|
||||
def process_response(self, request, resp):
|
||||
def nested_dict_values(d):
|
||||
for v in d.values():
|
||||
if isinstance(v, dict):
|
||||
yield from nested_dict_values(v)
|
||||
else:
|
||||
if isinstance(v, str):
|
||||
yield v
|
||||
|
||||
url = resolve(request.path_info)
|
||||
|
||||
if settings.DEBUG and resp.status_code >= 400:
|
||||
@@ -247,8 +256,6 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
# pages
|
||||
return resp
|
||||
|
||||
resp['X-XSS-Protection'] = '1'
|
||||
|
||||
# We just need to have a P3P, not matter whats in there
|
||||
# https://blogs.msdn.microsoft.com/ieinternals/2013/09/17/a-quick-look-at-p3p/
|
||||
# https://github.com/pretix/pretix/issues/765
|
||||
@@ -259,6 +266,14 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
if gs.settings.leaflet_tiles:
|
||||
img_src.append(gs.settings.leaflet_tiles[:gs.settings.leaflet_tiles.index("/", 10)].replace("{s}", "*"))
|
||||
|
||||
font_src = set()
|
||||
if hasattr(request, 'event'):
|
||||
for font in get_fonts(request.event, pdf_support_required=False).values():
|
||||
for path in list(nested_dict_values(font)):
|
||||
font_location = urlparse(path)
|
||||
if font_location.scheme and font_location.netloc:
|
||||
font_src.add('{}://{}'.format(font_location.scheme, font_location.netloc))
|
||||
|
||||
h = {
|
||||
'default-src': ["{static}"],
|
||||
'script-src': ['{static}'],
|
||||
@@ -267,7 +282,7 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
'style-src': ["{static}", "{media}"],
|
||||
'connect-src': ["{dynamic}", "{media}"],
|
||||
'img-src': ["{static}", "{media}", "data:"] + img_src,
|
||||
'font-src': ["{static}"],
|
||||
'font-src': ["{static}"] + list(font_src),
|
||||
'media-src': ["{static}", "data:"],
|
||||
# form-action is not only used to match on form actions, but also on URLs
|
||||
# form-actions redirect to. In the context of e.g. payment providers or
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.channels import get_all_sales_channel_types
|
||||
|
||||
|
||||
def set_sales_channels(apps, schema_editor):
|
||||
@@ -11,7 +11,7 @@ def set_sales_channels(apps, schema_editor):
|
||||
# Therefore, for existing events, we enable all sales channels
|
||||
Event_SettingsStore = apps.get_model('pretixbase', 'Event_SettingsStore')
|
||||
Event = apps.get_model('pretixbase', 'Event')
|
||||
all_sales_channels = "[" + ", ".join('"' + sc + '"' for sc in get_all_sales_channels()) + "]"
|
||||
all_sales_channels = "[" + ", ".join('"' + sc + '"' for sc in get_all_sales_channel_types()) + "]"
|
||||
batch_size = 1000
|
||||
Event_SettingsStore.objects.bulk_create([
|
||||
Event_SettingsStore(
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from django.db import migrations
|
||||
|
||||
import pretix.base.models.fields
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.channels import get_all_sales_channel_types
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -15,6 +15,6 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='sales_channels',
|
||||
field=pretix.base.models.fields.MultiStringField(default=list(get_all_sales_channels().keys())),
|
||||
field=pretix.base.models.fields.MultiStringField(default=list(get_all_sales_channel_types().keys())),
|
||||
),
|
||||
]
|
||||
|
||||
22
src/pretix/base/migrations/0255_item_unavail_modes.py
Normal file
22
src/pretix/base/migrations/0255_item_unavail_modes.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 4.2.4 on 2023-11-22 20:53
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("pretixbase", "0254_alter_logentry_organizer_link_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="item",
|
||||
name="available_from_mode",
|
||||
field=models.CharField(default="hide", max_length=16),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="item",
|
||||
name="available_until_mode",
|
||||
field=models.CharField(default="hide", max_length=16),
|
||||
)
|
||||
]
|
||||
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 4.2.4 on 2024-01-11 15:56
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("pretixbase", "0255_item_unavail_modes"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="itemvariation",
|
||||
name="available_from_mode",
|
||||
field=models.CharField(default="hide", max_length=16),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="itemvariation",
|
||||
name="available_until_mode",
|
||||
field=models.CharField(default="hide", max_length=16),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 4.2.9 on 2024-01-30 11:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0256_itemvariation_unavail_modes"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="item",
|
||||
name="default_price",
|
||||
field=models.DecimalField(decimal_places=2, default=0, max_digits=13),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
48
src/pretix/base/migrations/0258_uniq_indx.py
Normal file
48
src/pretix/base/migrations/0258_uniq_indx.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# Generated by Django 4.2.10 on 2024-03-15 09:59
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0257_item_default_price_not_null"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="order",
|
||||
name="organizer",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="orders",
|
||||
to="pretixbase.organizer",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="orderposition",
|
||||
name="organizer",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="order_positions",
|
||||
to="pretixbase.organizer",
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="order",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("organizer", "code"), name="order_organizer_code_uniq"
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="orderposition",
|
||||
constraint=models.UniqueConstraint(
|
||||
models.F("organizer"),
|
||||
models.F("secret"),
|
||||
name="orderposition_organizer_secret_uniq",
|
||||
),
|
||||
),
|
||||
]
|
||||
18
src/pretix/base/migrations/0259_team_require_2fa.py
Normal file
18
src/pretix/base/migrations/0259_team_require_2fa.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.10 on 2024-04-02 11:21
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0258_uniq_indx"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="team",
|
||||
name="require_2fa",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.2.10 on 2024-04-02 15:16
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0259_team_require_2fa"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterIndexTogether(
|
||||
name="reusablemedium",
|
||||
index_together=set(),
|
||||
),
|
||||
]
|
||||
48
src/pretix/base/migrations/0261_userknownloginsource.py
Normal file
48
src/pretix/base/migrations/0261_userknownloginsource.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# Generated by Django 4.2.10 on 2024-04-02 15:37
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.helpers.countries
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0260_alter_reusablemedium_index_together"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="UserKnownLoginSource",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False
|
||||
),
|
||||
),
|
||||
("agent_type", models.CharField(max_length=255, null=True)),
|
||||
("device_type", models.CharField(max_length=255, null=True)),
|
||||
("os_type", models.CharField(max_length=255, null=True)),
|
||||
(
|
||||
"country",
|
||||
pretix.helpers.countries.FastCountryField(
|
||||
countries=pretix.helpers.countries.CachedCountries,
|
||||
max_length=2,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
("last_seen", models.DateTimeField()),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="known_login_sources",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user