forked from CGM_Public/pretix_original
Compare commits
789 Commits
data-attr-
...
actions-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1c9a176d9 | ||
|
|
c7bcce0100 | ||
|
|
39c3aef7bc | ||
|
|
cf3087453c | ||
|
|
7a870ee521 | ||
|
|
3922290633 | ||
|
|
8aa13d7e3e | ||
|
|
22e9a6eb92 | ||
|
|
2b6f82502e | ||
|
|
a10bf2a939 | ||
|
|
a80b7087d9 | ||
|
|
4b143e98eb | ||
|
|
bdb8b597d0 | ||
|
|
b1c9f40bc8 | ||
|
|
a3b6a008b5 | ||
|
|
9ce05e5cb9 | ||
|
|
f306527981 | ||
|
|
3e17ff9faa | ||
|
|
2a16cd4655 | ||
|
|
d1078da5bf | ||
|
|
483e7bc4ad | ||
|
|
401218b0a3 | ||
|
|
19175258fd | ||
|
|
22c36b89da | ||
|
|
2697ed0c5d | ||
|
|
f81d820a02 | ||
|
|
f8df66e621 | ||
|
|
2d9bfc80dc | ||
|
|
17b2e95569 | ||
|
|
e49f938eb3 | ||
|
|
8d63906341 | ||
|
|
cfefa1aad0 | ||
|
|
1d16049dc5 | ||
|
|
8452899edd | ||
|
|
d67ebc0f80 | ||
|
|
0e87f03e1e | ||
|
|
868408ea55 | ||
|
|
fc75cd35f8 | ||
|
|
a3e2540331 | ||
|
|
99ce7effde | ||
|
|
0d645fc4c5 | ||
|
|
359df1f51e | ||
|
|
7607cc5d2f | ||
|
|
40c8d014df | ||
|
|
c10efc692d | ||
|
|
8f0a277c7b | ||
|
|
9dc38e42d8 | ||
|
|
bfd88d1496 | ||
|
|
be6bd501bd | ||
|
|
d160c9fd67 | ||
|
|
221f14cc21 | ||
|
|
1dda2eb4fb | ||
|
|
30f2e99020 | ||
|
|
8efe276ed0 | ||
|
|
61b25acdd2 | ||
|
|
6cc9529d9a | ||
|
|
cdc5401dc2 | ||
|
|
1334a570e4 | ||
|
|
7a66aea2cb | ||
|
|
ee77a5e447 | ||
|
|
827e127568 | ||
|
|
ce0e0d7fd1 | ||
|
|
152a956dc5 | ||
|
|
68e2c355e6 | ||
|
|
171615558f | ||
|
|
a1765910ea | ||
|
|
417277958b | ||
|
|
0d50494e89 | ||
|
|
c6f634ce72 | ||
|
|
adc78c14ab | ||
|
|
b4ca2bdbb4 | ||
|
|
9a7ff592af | ||
|
|
548b54cca6 | ||
|
|
e736791446 | ||
|
|
7bd945b2e6 | ||
|
|
a07d5aaf05 | ||
|
|
0cf1a32902 | ||
|
|
be6aae8577 | ||
|
|
fe80f5fb78 | ||
|
|
a2c15ad89e | ||
|
|
cab0f37830 | ||
|
|
0423980058 | ||
|
|
63983b1b68 | ||
|
|
61241c2a1e | ||
|
|
4069c61054 | ||
|
|
9bf4fb2d0f | ||
|
|
ff910f293f | ||
|
|
74f7bec617 | ||
|
|
467a35e353 | ||
|
|
770c13a4f0 | ||
|
|
5373d4d8ba | ||
|
|
42e673b5f6 | ||
|
|
7af2f2a87b | ||
|
|
e408521769 | ||
|
|
8ed0d36346 | ||
|
|
14cbe99667 | ||
|
|
b059995eff | ||
|
|
100e8d0a4b | ||
|
|
eb92e4d8e6 | ||
|
|
32d6ded003 | ||
|
|
aa07533693 | ||
|
|
e7d01f91a6 | ||
|
|
9616369f07 | ||
|
|
af606090ba | ||
|
|
931f3eca1b | ||
|
|
36f306120e | ||
|
|
a3ba0c97e9 | ||
|
|
484d24b66c | ||
|
|
2d39d3cc8e | ||
|
|
78b1adf423 | ||
|
|
c3eedcc396 | ||
|
|
682c328390 | ||
|
|
5230827f5e | ||
|
|
dad9915435 | ||
|
|
a9d2c1eb34 | ||
|
|
66fe45a478 | ||
|
|
24e2b1b9ab | ||
|
|
eebdce80cd | ||
|
|
09af95ec20 | ||
|
|
1ade674beb | ||
|
|
76ff59f9c2 | ||
|
|
0986522c2f | ||
|
|
91f4e731da | ||
|
|
98709286c6 | ||
|
|
667c2555b2 | ||
|
|
6f5acb1ca7 | ||
|
|
65ec3e3fd6 | ||
|
|
1a8d0a973d | ||
|
|
3c94631405 | ||
|
|
1dda7732a5 | ||
|
|
33accf5f99 | ||
|
|
be2efd9df2 | ||
|
|
fe69137a4e | ||
|
|
7ccfb3a27a | ||
|
|
b7205622dc | ||
|
|
44da5b81b1 | ||
|
|
5a058342a6 | ||
|
|
2d15dc7ce5 | ||
|
|
dd4ccc864e | ||
|
|
b812f0affe | ||
|
|
2af4183ce6 | ||
|
|
8ac0b93ca5 | ||
|
|
51a1193f32 | ||
|
|
002da2c9b7 | ||
|
|
9a2ebe4e95 | ||
|
|
bc6da2512a | ||
|
|
6378dc69b8 | ||
|
|
2b53d04a19 | ||
|
|
7efe7b5ff7 | ||
|
|
ae5464d486 | ||
|
|
67fec8d1f6 | ||
|
|
95a081676b | ||
|
|
7228a6304d | ||
|
|
04b9134e36 | ||
|
|
2e0769bc41 | ||
|
|
4d2f854710 | ||
|
|
b9ac9496d2 | ||
|
|
a975f5dc50 | ||
|
|
4ea1f6284a | ||
|
|
a01d105829 | ||
|
|
b1bfa1acee | ||
|
|
0b4e99c2d8 | ||
|
|
0cdce7a9cd | ||
|
|
464f625301 | ||
|
|
0c1072503c | ||
|
|
9ead82839a | ||
|
|
c346e3a7f4 | ||
|
|
a26f219faf | ||
|
|
74fb8e7d0c | ||
|
|
b9dbeef1ef | ||
|
|
54079797d2 | ||
|
|
02a4ed4be2 | ||
|
|
7f7c95aedb | ||
|
|
47af20d417 | ||
|
|
91e69f793d | ||
|
|
43e24ff88c | ||
|
|
fa3f6def82 | ||
|
|
34469bc222 | ||
|
|
d0364300b5 | ||
|
|
55bc55cc53 | ||
|
|
0ee5511cca | ||
|
|
192699a2c2 | ||
|
|
b8255bc7a0 | ||
|
|
d7f0c14fdc | ||
|
|
3f9ba2f223 | ||
|
|
3f811cc020 | ||
|
|
03f3203a82 | ||
|
|
59901603c6 | ||
|
|
aefb38cdd7 | ||
|
|
aed3ccd2dd | ||
|
|
893d115948 | ||
|
|
8e87cf67c7 | ||
|
|
8972715252 | ||
|
|
1879e440a7 | ||
|
|
f819f0c316 | ||
|
|
a1db13b75e | ||
|
|
6087665775 | ||
|
|
a6f93b6cf0 | ||
|
|
b96374fcf6 | ||
|
|
eb2ad48089 | ||
|
|
64dac504ca | ||
|
|
cf15a08712 | ||
|
|
9197274528 | ||
|
|
d19176ab41 | ||
|
|
8d8abbd941 | ||
|
|
5142c62e6e | ||
|
|
7f7223fcdc | ||
|
|
cdde688964 | ||
|
|
233bcaf00e | ||
|
|
0a5f3e6dd5 | ||
|
|
446d24553e | ||
|
|
45c32bcb05 | ||
|
|
5a5090604a | ||
|
|
2b370bde6d | ||
|
|
024a223ec7 | ||
|
|
022f44ad00 | ||
|
|
a682eab18e | ||
|
|
6721762a3f | ||
|
|
ad443d0eb6 | ||
|
|
ececd3e572 | ||
|
|
ffc4a76b11 | ||
|
|
4beb0c2e30 | ||
|
|
48e161d2d4 | ||
|
|
dc1973f4ff | ||
|
|
a0b046d204 | ||
|
|
0032f83d93 | ||
|
|
f312200881 | ||
|
|
9946da57c2 | ||
|
|
11e04ea3f2 | ||
|
|
9cef63d641 | ||
|
|
cb833cc6da | ||
|
|
5320a69c27 | ||
|
|
510ca67107 | ||
|
|
13720e731e | ||
|
|
78cfbd6460 | ||
|
|
a65f94fa85 | ||
|
|
288f73b735 | ||
|
|
ad33785f4c | ||
|
|
bbc175d3d6 | ||
|
|
2876ff5549 | ||
|
|
ed9caa04fc | ||
|
|
83a8fcaa47 | ||
|
|
858a448db5 | ||
|
|
58b803539b | ||
|
|
6c92c5bacf | ||
|
|
f0089f20fb | ||
|
|
cb2d056afd | ||
|
|
afb115c9a2 | ||
|
|
bb92ffe4eb | ||
|
|
8da8e2f43d | ||
|
|
cab360bdb6 | ||
|
|
c6a2ae3783 | ||
|
|
26ec9dcf6c | ||
|
|
c0832098ef | ||
|
|
fa3ac69b8e | ||
|
|
17f1d571b0 | ||
|
|
a692940397 | ||
|
|
7f2ec51c64 | ||
|
|
aba59a391c | ||
|
|
a819b8bb71 | ||
|
|
8a3b18fbd2 | ||
|
|
dd444299f0 | ||
|
|
3ee5e9cfbc | ||
|
|
f660f35766 | ||
|
|
42e26738e5 | ||
|
|
7c43f115b2 | ||
|
|
f055a598ce | ||
|
|
9138464896 | ||
|
|
479f51a84c | ||
|
|
a3ac54d419 | ||
|
|
b2841e5c61 | ||
|
|
3009f50d51 | ||
|
|
ff3a49ab2a | ||
|
|
19f3fbc7e8 | ||
|
|
bb9b9ac9aa | ||
|
|
d7f6befb5b | ||
|
|
2287be2009 | ||
|
|
0480b6873d | ||
|
|
711f08c9e8 | ||
|
|
d18914fcca | ||
|
|
2411144262 | ||
|
|
2f02d35a52 | ||
|
|
71e82fda81 | ||
|
|
ca3802da90 | ||
|
|
2c68b9e895 | ||
|
|
01092498f4 | ||
|
|
fd841ed66d | ||
|
|
04cbccb536 | ||
|
|
b8ea93de1e | ||
|
|
c49f42301c | ||
|
|
2ae0a16e67 | ||
|
|
6b06fdf822 | ||
|
|
ea3f4e5f62 | ||
|
|
d71c23f7e0 | ||
|
|
5ed7b0032b | ||
|
|
a77f2d01a7 | ||
|
|
ca4f511cde | ||
|
|
83b1c2ea7e | ||
|
|
c91eb2e20d | ||
|
|
bfb480a288 | ||
|
|
22e2143623 | ||
|
|
9e61f7f978 | ||
|
|
092de9e3c4 | ||
|
|
f0822d3c27 | ||
|
|
6fc47ca3b6 | ||
|
|
3716a686f5 | ||
|
|
9c0c77958e | ||
|
|
6154a44274 | ||
|
|
d5aff10297 | ||
|
|
846e39a652 | ||
|
|
75d37b2a37 | ||
|
|
2a0c3da8c4 | ||
|
|
fb7f4d1160 | ||
|
|
5c8817f0c3 | ||
|
|
7663bf7994 | ||
|
|
ea8f74f8aa | ||
|
|
7b34701449 | ||
|
|
06f4bfea24 | ||
|
|
e2862a98a0 | ||
|
|
82f4feadc3 | ||
|
|
906222c7d3 | ||
|
|
7ce2089ca8 | ||
|
|
2133584ed2 | ||
|
|
c3c50d7205 | ||
|
|
4f7a41a4b2 | ||
|
|
93fd79ab33 | ||
|
|
59595c2f10 | ||
|
|
91c6d09f0b | ||
|
|
570a818129 | ||
|
|
d63e2ebe2d | ||
|
|
884c97d62a | ||
|
|
032e958a00 | ||
|
|
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 |
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -26,12 +26,12 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.11"]
|
python-version: ["3.11"]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v1
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- uses: actions/cache@v1
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
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
|
name: Spellcheck
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python 3.11
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v1
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
python-version: 3.11
|
||||||
- uses: actions/cache@v1
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
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
|
runs-on: ubuntu-22.04
|
||||||
name: Check gettext syntax
|
name: Check gettext syntax
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python 3.11
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v1
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
python-version: 3.11
|
||||||
- uses: actions/cache@v1
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||||
@@ -48,12 +48,12 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
name: Spellcheck
|
name: Spellcheck
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python 3.11
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v1
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
python-version: 3.11
|
||||||
- uses: actions/cache@v1
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
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
|
name: isort
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python 3.11
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v1
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
python-version: 3.11
|
||||||
- uses: actions/cache@v1
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||||
@@ -43,12 +43,12 @@ jobs:
|
|||||||
name: flake8
|
name: flake8
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python 3.11
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v1
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
python-version: 3.11
|
||||||
- uses: actions/cache@v1
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||||
@@ -63,9 +63,9 @@ jobs:
|
|||||||
name: licenseheaders
|
name: licenseheaders
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python 3.11
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v1
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
python-version: 3.11
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
|
|||||||
10
.github/workflows/tests.yml
vendored
10
.github/workflows/tests.yml
vendored
@@ -5,7 +5,6 @@ on:
|
|||||||
branches: [ master ]
|
branches: [ master ]
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- 'doc/**'
|
- 'doc/**'
|
||||||
- 'src/pretix/locale/**'
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master ]
|
branches: [ master ]
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
@@ -23,6 +22,7 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
name: Tests
|
name: Tests
|
||||||
strategy:
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.9", "3.10", "3.11"]
|
python-version: ["3.9", "3.10", "3.11"]
|
||||||
database: [sqlite, postgres]
|
database: [sqlite, postgres]
|
||||||
@@ -32,7 +32,7 @@ jobs:
|
|||||||
- database: sqlite
|
- database: sqlite
|
||||||
python-version: "3.10"
|
python-version: "3.10"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: harmon758/postgresql-action@v1
|
- uses: harmon758/postgresql-action@v1
|
||||||
with:
|
with:
|
||||||
postgresql version: '15'
|
postgresql version: '15'
|
||||||
@@ -41,10 +41,10 @@ jobs:
|
|||||||
postgresql password: 'postgres'
|
postgresql password: 'postgres'
|
||||||
if: matrix.database == 'postgres'
|
if: matrix.database == 'postgres'
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v1
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- uses: actions/cache@v1
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||||
@@ -65,7 +65,7 @@ jobs:
|
|||||||
run: make all compress
|
run: make all compress
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
run: PRETIX_CONFIG_FILE=tests/travis_${{ matrix.database }}.cfg py.test -n 3 -p no:sugar --cov=./ --cov-report=xml --reruns 3 tests --maxfail=100
|
run: PRETIX_CONFIG_FILE=tests/travis_${{ matrix.database }}.cfg py.test -vv -n 3 -p no:sugar --cov=./ --cov-report=xml --reruns 3 tests --maxfail=100
|
||||||
- name: Run concurrency tests
|
- name: Run concurrency tests
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
run: PRETIX_CONFIG_FILE=tests/travis_${{ matrix.database }}.cfg py.test tests/concurrency_tests/ --reruns 0 --reuse-db
|
run: PRETIX_CONFIG_FILE=tests/travis_${{ matrix.database }}.cfg py.test tests/concurrency_tests/ --reruns 0 --reuse-db
|
||||||
|
|||||||
@@ -1,29 +1,30 @@
|
|||||||
before_script:
|
|
||||||
tests:
|
tests:
|
||||||
|
image:
|
||||||
|
name: pretix/ci-image
|
||||||
stage: test
|
stage: test
|
||||||
|
before_script:
|
||||||
|
- pip install -U pip uv
|
||||||
|
- uv pip install --system -U wheel setuptools
|
||||||
script:
|
script:
|
||||||
- virtualenv env
|
- uv pip install --system -e ".[dev]"
|
||||||
- source env/bin/activate
|
|
||||||
- pip install -U pip wheel setuptools
|
|
||||||
- XDG_CACHE_HOME=/cache pip3 install -e ".[dev]"
|
|
||||||
- cd src
|
- cd src
|
||||||
- python manage.py check
|
- python manage.py check
|
||||||
- make all compress
|
- make all compress
|
||||||
- py.test --reruns 3 -n 3 tests
|
- PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg py.test --reruns 3 -n 3 tests --maxfail=100
|
||||||
tags:
|
|
||||||
- python3
|
|
||||||
except:
|
except:
|
||||||
- pypi
|
- pypi
|
||||||
pypi:
|
pypi:
|
||||||
stage: release
|
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:
|
script:
|
||||||
- cp /keys/.pypirc ~/.pypirc
|
- uv pip install --system -e ".[dev]"
|
||||||
- virtualenv env
|
|
||||||
- source env/bin/activate
|
|
||||||
- pip install -U pip wheel setuptools check-manifest twine
|
|
||||||
- XDG_CACHE_HOME=/cache pip3 install -e ".[dev]"
|
|
||||||
- python setup.py sdist
|
- 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 migrate
|
||||||
- python -m pretix check
|
- python -m pretix check
|
||||||
- cd src
|
- cd src
|
||||||
@@ -33,13 +34,12 @@ pypi:
|
|||||||
- python -m build
|
- python -m build
|
||||||
- twine check dist/*
|
- twine check dist/*
|
||||||
- twine upload dist/*
|
- twine upload dist/*
|
||||||
tags:
|
|
||||||
- python3
|
|
||||||
only:
|
only:
|
||||||
- pypi
|
- pypi
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- src/dist/
|
- src/dist/
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- test
|
- test
|
||||||
- build
|
- build
|
||||||
|
|||||||
1
.node-version
Normal file
1
.node-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
17
|
||||||
@@ -10,6 +10,8 @@ recursive-include src/pretix/helpers/locale *
|
|||||||
recursive-include src/pretix/base/templates *
|
recursive-include src/pretix/base/templates *
|
||||||
recursive-include src/pretix/control/templates *
|
recursive-include src/pretix/control/templates *
|
||||||
recursive-include src/pretix/presale/templates *
|
recursive-include src/pretix/presale/templates *
|
||||||
|
recursive-include src/pretix/plugins/autocheckin/templates *
|
||||||
|
recursive-include src/pretix/plugins/autocheckin/static *
|
||||||
recursive-include src/pretix/plugins/banktransfer/templates *
|
recursive-include src/pretix/plugins/banktransfer/templates *
|
||||||
recursive-include src/pretix/plugins/banktransfer/static *
|
recursive-include src/pretix/plugins/banktransfer/static *
|
||||||
recursive-include src/pretix/plugins/manualpayment/templates *
|
recursive-include src/pretix/plugins/manualpayment/templates *
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ if [ "$1" == "taskworker" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$1" == "upgrade" ]; then
|
if [ "$1" == "upgrade" ]; then
|
||||||
exec python3 -m pretix updatestyles
|
exec python3 -m pretix updateassets
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exec python3 -m pretix "$@"
|
exec python3 -m pretix "$@"
|
||||||
|
|||||||
@@ -52,10 +52,18 @@ Example::
|
|||||||
``currency``
|
``currency``
|
||||||
The default currency as a three-letter code. Defaults to ``EUR``.
|
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``
|
``datadir``
|
||||||
The local path to a data directory that will be used for storing user uploads and similar
|
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``.
|
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``
|
``plugins_default``
|
||||||
A comma-separated list of plugins that are enabled by default for all new events.
|
A comma-separated list of plugins that are enabled by default for all new events.
|
||||||
Defaults to ``pretix.plugins.sendmail,pretix.plugins.statistics``.
|
Defaults to ``pretix.plugins.sendmail,pretix.plugins.statistics``.
|
||||||
@@ -89,8 +97,9 @@ Example::
|
|||||||
Defaults to ``off``.
|
Defaults to ``off``.
|
||||||
|
|
||||||
``obligatory_2fa``
|
``obligatory_2fa``
|
||||||
Enables or disables obligatory usage of Two-Factor Authentication for users of the pretix backend.
|
Enables or disables obligatory usage of two-factor authentication for users of the pretix backend.
|
||||||
Defaults to ``False``
|
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``
|
``trust_x_forwarded_for``
|
||||||
Specifies whether the ``X-Forwarded-For`` header can be trusted. Only set to ``on`` if you have a reverse
|
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
|
host=localhost
|
||||||
port=3306
|
port=3306
|
||||||
advisory_lock_index=1
|
advisory_lock_index=1
|
||||||
|
disable_server_side_cursors=0
|
||||||
sslmode=require
|
sslmode=require
|
||||||
sslrootcert=/etc/pretix/postgresql-ca.crt
|
sslrootcert=/etc/pretix/postgresql-ca.crt
|
||||||
sslcert=/etc/pretix/postgresql-client-crt.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,
|
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).
|
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``
|
``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.
|
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.
|
||||||
|
|
||||||
@@ -279,6 +294,10 @@ Example::
|
|||||||
setting is not provided, pretix will generate a random secret on the first start
|
setting is not provided, pretix will generate a random secret on the first start
|
||||||
and will store it in the filesystem for later usage.
|
and will store it in the filesystem for later usage.
|
||||||
|
|
||||||
|
``secret_fallback0`` ... ``secret_fallback9``
|
||||||
|
Prior versions of the secret to be used by Django for signing and verification purposes that will still
|
||||||
|
be accepted but no longer be used for new signing.
|
||||||
|
|
||||||
``debug``
|
``debug``
|
||||||
Whether or not to run in debug mode. Default is ``False``.
|
Whether or not to run in debug mode. Default is ``False``.
|
||||||
|
|
||||||
|
|||||||
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)$ pip3 install -U "git+https://github.com/pretix/pretix.git#egg=pretix"
|
||||||
(venv)$ python -m pretix migrate
|
(venv)$ python -m pretix migrate
|
||||||
(venv)$ python -m pretix rebuild
|
(venv)$ python -m pretix rebuild
|
||||||
(venv)$ python -m pretix updatestyles
|
(venv)$ python -m pretix updateassets
|
||||||
# systemctl restart pretix-web pretix-worker
|
# systemctl restart pretix-web pretix-worker
|
||||||
|
|
||||||
Docker installation
|
Docker installation
|
||||||
|
|||||||
@@ -14,3 +14,4 @@ for your needs.
|
|||||||
manual_smallscale
|
manual_smallscale
|
||||||
dev_version
|
dev_version
|
||||||
enterprise
|
enterprise
|
||||||
|
community
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ Package dependencies
|
|||||||
To build and run pretix, you will need the following debian packages::
|
To build and run pretix, you will need the following debian packages::
|
||||||
|
|
||||||
# apt-get install git build-essential python3-dev python3-venv python3 python3-pip \
|
# apt-get install git build-essential python3-dev python3-venv python3 python3-pip \
|
||||||
python3-dev libxml2-dev libxslt1-dev libffi-dev zlib1g-dev libssl-dev \
|
libxml2-dev libxslt1-dev libffi-dev zlib1g-dev libssl-dev \
|
||||||
gettext libpq-dev libjpeg-dev libopenjp2-7-dev
|
gettext libpq-dev libjpeg-dev libopenjp2-7-dev
|
||||||
|
|
||||||
Config file
|
Config file
|
||||||
@@ -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
|
actually install pretix, we will create a virtual environment to isolate the python packages from your global
|
||||||
python installation::
|
python installation::
|
||||||
|
|
||||||
|
# sudo -u pretix -s
|
||||||
$ python3 -m venv /var/pretix/venv
|
$ python3 -m venv /var/pretix/venv
|
||||||
$ source /var/pretix/venv/bin/activate
|
$ source /var/pretix/venv/bin/activate
|
||||||
(venv)$ pip3 install -U pip setuptools wheel
|
(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::
|
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
|
$ source /var/pretix/venv/bin/activate
|
||||||
(venv)$ pip3 install -U --upgrade-strategy eager pretix gunicorn
|
(venv)$ pip3 install -U --upgrade-strategy eager pretix gunicorn
|
||||||
(venv)$ python -m pretix migrate
|
(venv)$ python -m pretix migrate
|
||||||
(venv)$ python -m pretix rebuild
|
(venv)$ python -m pretix rebuild
|
||||||
(venv)$ python -m pretix updatestyles
|
(venv)$ python -m pretix updateassets
|
||||||
# systemctl restart pretix-web pretix-worker
|
# 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
|
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 migrate
|
||||||
(venv)$ python -m pretix rebuild
|
(venv)$ python -m pretix rebuild
|
||||||
(venv)$ python -m pretix updatestyles
|
(venv)$ python -m pretix updateassets
|
||||||
# systemctl restart pretix-web pretix-worker
|
# 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
|
.. _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
|
pretix_celery_tasks_queued_age_seconds
|
||||||
The age of the longest-waiting in the worker queue in seconds, labeled with ``queue``.
|
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/
|
.. _metric types: https://prometheus.io/docs/concepts/metric_types/
|
||||||
.. _Prometheus: https://prometheus.io/
|
.. _Prometheus: https://prometheus.io/
|
||||||
.. _cProfile: https://docs.python.org/3/library/profile.html
|
.. _cProfile: https://docs.python.org/3/library/profile.html
|
||||||
|
|||||||
@@ -73,4 +73,11 @@ This release includes a migration that changes retroactively fills an `organizer
|
|||||||
`pretixbase_logentry`. If you have a large database, the migration step of the upgrade might take significantly
|
`pretixbase_logentry`. If you have a large database, the migration step of the upgrade might take significantly
|
||||||
longer than usual, so plan the update accordingly.
|
longer than usual, so plan the update accordingly.
|
||||||
|
|
||||||
|
Upgrade to 2024.7.0 or newer
|
||||||
|
"""""""""""""""""""""""""""""
|
||||||
|
|
||||||
|
This release includes a migration that changes how sales channels are referred on orders.
|
||||||
|
If you have a large database, the migration step of the upgrade might take significantly longer than usual, so plan
|
||||||
|
the update accordingly.
|
||||||
|
|
||||||
.. _blog: https://pretix.eu/about/en/blog/
|
.. _blog: https://pretix.eu/about/en/blog/
|
||||||
|
|||||||
@@ -249,7 +249,10 @@ You can get three response codes:
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"event": "democon",
|
"event": {
|
||||||
|
"name": "Demo Conference",
|
||||||
|
"slug": "democon"
|
||||||
|
},
|
||||||
"subevent": 23,
|
"subevent": 23,
|
||||||
"checkinlist": 5
|
"checkinlist": 5
|
||||||
}
|
}
|
||||||
|
|||||||
259
doc/api/resources/auto_checkin_rules.rst
Normal file
259
doc/api/resources/auto_checkin_rules.rst
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
.. _rest-autocheckinrules:
|
||||||
|
|
||||||
|
Auto check-in rules
|
||||||
|
===================
|
||||||
|
|
||||||
|
This feature requires the bundled ``pretix.plugins.autocheckin`` plugin to be active for the event in order to work properly.
|
||||||
|
|
||||||
|
Resource description
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
Auto check-in rules specify that tickets should under specific conditions automatically be considered checked in after
|
||||||
|
they have been purchased.
|
||||||
|
|
||||||
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
Field Type Description
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
id integer Internal ID of the rule
|
||||||
|
list integer ID of the check-in list to check the ticket in on. If
|
||||||
|
``None``, the system will select all matching check-in lists.
|
||||||
|
mode string ``"placed"`` if the rule should be evaluated right after
|
||||||
|
an order has been created, ``"paid"`` if the rule should
|
||||||
|
be evaluated after the order has been fully paid.
|
||||||
|
all_sales_channels boolean If ``true`` (default), the rule applies to tickets sold on all sales channels.
|
||||||
|
limit_sales_channels list of strings List of sales channel identifiers the rule should apply to
|
||||||
|
if ``all_sales_channels`` is ``false``.
|
||||||
|
all_products boolean If ``true`` (default), the rule affects all products and variations.
|
||||||
|
limit_products list of integers List of item IDs, if ``all_products`` is not set. If the
|
||||||
|
product listed here has variations, all variations will be matched.
|
||||||
|
limit_variations list of integers List of product variation IDs, if ``all_products`` is not set.
|
||||||
|
The parent product does not need to be part of ``limit_products``.
|
||||||
|
all_payment_methods boolean If ``true`` (default), the rule applies to tickets paid with all payment methods.
|
||||||
|
limit_payment_methods list of strings List of payment method identifiers the rule should apply to
|
||||||
|
if ``all_payment_methods`` is ``false``.
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
.. versionadded:: 2024.7
|
||||||
|
|
||||||
|
Endpoints
|
||||||
|
---------
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/auto_checkin_rules/
|
||||||
|
|
||||||
|
Returns a list of all rules configured for an event.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/auto_checkin_rules/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"list": 12345,
|
||||||
|
"mode": "placed",
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
|
"all_products": False,
|
||||||
|
"limit_products": [2, 3],
|
||||||
|
"limit_variations": [456],
|
||||||
|
"all_payment_methods": true,
|
||||||
|
"limit_payment_methods": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
:query page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:param organizer: The ``slug`` field of a valid organizer
|
||||||
|
:param event: The ``slug`` field of the event to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/auto_checkin_rules/(id)/
|
||||||
|
|
||||||
|
Returns information on one rule, identified by its ID.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/auto_checkin_rules/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"list": 12345,
|
||||||
|
"mode": "placed",
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
|
"all_products": False,
|
||||||
|
"limit_products": [2, 3],
|
||||||
|
"limit_variations": [456],
|
||||||
|
"all_payment_methods": true,
|
||||||
|
"limit_payment_methods": []
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param event: The ``slug`` field of the event to fetch
|
||||||
|
:param id: The ``id`` field of the rule to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/auto_checkin_rules/
|
||||||
|
|
||||||
|
Create a new rule.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/auto_checkin_rules/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
Content-Length: 166
|
||||||
|
|
||||||
|
{
|
||||||
|
"list": 12345,
|
||||||
|
"mode": "placed",
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
|
"all_products": False,
|
||||||
|
"limit_products": [2, 3],
|
||||||
|
"limit_variations": [456],
|
||||||
|
"all_payment_methods": true,
|
||||||
|
"limit_payment_methods": []
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"list": 12345,
|
||||||
|
"mode": "placed",
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
|
"all_products": False,
|
||||||
|
"limit_products": [2, 3],
|
||||||
|
"limit_variations": [456],
|
||||||
|
"all_payment_methods": true,
|
||||||
|
"limit_payment_methods": []
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to create a rule for
|
||||||
|
:param event: The ``slug`` field of the event to create a rule for
|
||||||
|
:statuscode 201: no error
|
||||||
|
:statuscode 400: The rule could not be created due to invalid submitted data.
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create rules.
|
||||||
|
|
||||||
|
|
||||||
|
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/auto_checkin_rules/(id)/
|
||||||
|
|
||||||
|
Update a rule. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
|
||||||
|
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
|
||||||
|
want to change.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
PATCH /api/v1/organizers/bigevents/events/sampleconf/auto_checkin_rules/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
Content-Length: 34
|
||||||
|
|
||||||
|
{
|
||||||
|
"mode": "paid",
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: text/javascript
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"list": 12345,
|
||||||
|
"mode": "placed",
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
|
"all_products": False,
|
||||||
|
"limit_products": [2, 3],
|
||||||
|
"limit_variations": [456],
|
||||||
|
"all_payment_methods": true,
|
||||||
|
"limit_payment_methods": []
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param event: The ``slug`` field of the event to modify
|
||||||
|
:param id: The ``id`` field of the rule to modify
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 400: The rule could not be modified due to invalid submitted data.
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to change it.
|
||||||
|
|
||||||
|
|
||||||
|
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/auto_checkin_rules/(id)/
|
||||||
|
|
||||||
|
Delete a rule.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
DELETE /api/v1/organizers/bigevents/events/sampleconf/auto_checkin_rules/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 204 No Content
|
||||||
|
Vary: Accept
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param event: The ``slug`` field of the event to modify
|
||||||
|
:param id: The ``id`` field of the rule to delete
|
||||||
|
:statuscode 204: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to change it **or** this rule cannot be deleted since it is currently in use.
|
||||||
@@ -40,6 +40,11 @@ answers list of objects Answers to user
|
|||||||
seat objects The assigned seat (or ``null``)
|
seat objects The assigned seat (or ``null``)
|
||||||
├ id integer Internal ID of the seat instance
|
├ id integer Internal ID of the seat instance
|
||||||
├ name string Human-readable seat name
|
├ 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
|
└ seat_guid string Identifier of the seat within the seating plan
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,22 @@ position integer An integer, use
|
|||||||
is_addon boolean If ``true``, items within this category are not on sale
|
is_addon boolean If ``true``, items within this category are not on sale
|
||||||
on their own but the category provides a source for
|
on their own but the category provides a source for
|
||||||
defining add-ons for other products.
|
defining add-ons for other products.
|
||||||
|
cross_selling_mode string If ``null``, cross-selling is disabled for this category.
|
||||||
|
If ``"only"``, it is only visible in the cross-selling
|
||||||
|
step.
|
||||||
|
If ``"both"``, it is visible on the normal index page
|
||||||
|
as well.
|
||||||
|
Only available if ``is_addon`` is ``false``.
|
||||||
|
cross_selling_condition string Only relevant if ``cross_selling_mode`` is not ``null``.
|
||||||
|
If ``"always"``, always show in cross-selling step.
|
||||||
|
If ``"products"``, only show if the cart contains one of
|
||||||
|
the products listed in ``cross_selling_match_products``.
|
||||||
|
If ``"discounts"``, only show products that qualify for
|
||||||
|
a discount according to discount rules.
|
||||||
|
cross_selling_match_products list of integer Only relevant if ``cross_selling_condition`` is
|
||||||
|
``"products"``. Internal ID of the items of which at
|
||||||
|
least one needs to be in the cart for this category to
|
||||||
|
be shown.
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|
||||||
@@ -60,7 +76,10 @@ Endpoints
|
|||||||
"internal_name": "",
|
"internal_name": "",
|
||||||
"description": {"en": "Tickets are what you need to get in."},
|
"description": {"en": "Tickets are what you need to get in."},
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"is_addon": false
|
"is_addon": false,
|
||||||
|
"cross_selling_mode": null,
|
||||||
|
"cross_selling_condition": null,
|
||||||
|
"cross_selling_match_products": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -102,7 +121,10 @@ Endpoints
|
|||||||
"internal_name": "",
|
"internal_name": "",
|
||||||
"description": {"en": "Tickets are what you need to get in."},
|
"description": {"en": "Tickets are what you need to get in."},
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"is_addon": false
|
"is_addon": false,
|
||||||
|
"cross_selling_mode": null,
|
||||||
|
"cross_selling_condition": null,
|
||||||
|
"cross_selling_match_products": []
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -130,7 +152,10 @@ Endpoints
|
|||||||
"internal_name": "",
|
"internal_name": "",
|
||||||
"description": {"en": "Tickets are what you need to get in."},
|
"description": {"en": "Tickets are what you need to get in."},
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"is_addon": false
|
"is_addon": false,
|
||||||
|
"cross_selling_mode": null,
|
||||||
|
"cross_selling_condition": null,
|
||||||
|
"cross_selling_match_products": []
|
||||||
}
|
}
|
||||||
|
|
||||||
**Example response**:
|
**Example response**:
|
||||||
@@ -147,7 +172,10 @@ Endpoints
|
|||||||
"internal_name": "",
|
"internal_name": "",
|
||||||
"description": {"en": "Tickets are what you need to get in."},
|
"description": {"en": "Tickets are what you need to get in."},
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"is_addon": false
|
"is_addon": false,
|
||||||
|
"cross_selling_mode": null,
|
||||||
|
"cross_selling_condition": null,
|
||||||
|
"cross_selling_match_products": []
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer of the event to create a category for
|
:param organizer: The ``slug`` field of the organizer of the event to create a category for
|
||||||
@@ -193,7 +221,10 @@ Endpoints
|
|||||||
"internal_name": "",
|
"internal_name": "",
|
||||||
"description": {"en": "Tickets are what you need to get in."},
|
"description": {"en": "Tickets are what you need to get in."},
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"is_addon": true
|
"is_addon": true,
|
||||||
|
"cross_selling_mode": null,
|
||||||
|
"cross_selling_condition": null,
|
||||||
|
"cross_selling_match_products": []
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to modify
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ position_count integer Number of ticke
|
|||||||
checkin_count integer Number of check-ins performed on this list (read-only).
|
checkin_count integer Number of check-ins performed on this list (read-only).
|
||||||
include_pending boolean If ``true``, the check-in list also contains tickets from orders in pending state.
|
include_pending boolean If ``true``, the check-in list also contains tickets from orders in pending state.
|
||||||
auto_checkin_sales_channels list of strings All items on the check-in list will be automatically marked as checked-in when purchased through any of the listed sales channels.
|
auto_checkin_sales_channels list of strings All items on the check-in list will be automatically marked as checked-in when purchased through any of the listed sales channels.
|
||||||
|
**Deprecated, will be removed in pretix 2024.10.** Use :ref:`rest-autocheckinrules`: instead.
|
||||||
allow_multiple_entries boolean If ``true``, subsequent scans of a ticket on this list should not show a warning but instead be stored as an additional check-in.
|
allow_multiple_entries boolean If ``true``, subsequent scans of a ticket on this list should not show a warning but instead be stored as an additional check-in.
|
||||||
allow_entry_after_exit boolean If ``true``, subsequent scans of a ticket on this list are valid if the last scan of the ticket was an exit scan.
|
allow_entry_after_exit boolean If ``true``, subsequent scans of a ticket on this list are valid if the last scan of the ticket was an exit scan.
|
||||||
rules object Custom check-in logic. The contents of this field are currently not considered a stable API and modifications through the API are highly discouraged.
|
rules object Custom check-in logic. The contents of this field are currently not considered a stable API and modifications through the API are highly discouraged.
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ external_identifier string External ID of
|
|||||||
the API, but is read-only for customers created through a
|
the API, but is read-only for customers created through a
|
||||||
SSO integration.
|
SSO integration.
|
||||||
email string Customer email address
|
email string Customer email address
|
||||||
|
phone string Customer phone number
|
||||||
name string Name of this customer (or ``null``)
|
name string Name of this customer (or ``null``)
|
||||||
name_parts object of strings Decomposition of name (i.e. given name, family name)
|
name_parts object of strings Decomposition of name (i.e. given name, family name)
|
||||||
is_active boolean Whether this account is active
|
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.
|
Passwords can now be set through the API during customer creation.
|
||||||
|
|
||||||
|
.. versionchanged:: 2024.3
|
||||||
|
|
||||||
|
The attribute ``phone`` has been added.
|
||||||
|
|
||||||
Endpoints
|
Endpoints
|
||||||
---------
|
---------
|
||||||
|
|
||||||
@@ -71,6 +76,7 @@ Endpoints
|
|||||||
"identifier": "8WSAJCJ",
|
"identifier": "8WSAJCJ",
|
||||||
"external_identifier": null,
|
"external_identifier": null,
|
||||||
"email": "customer@example.org",
|
"email": "customer@example.org",
|
||||||
|
"phone": "+493012345678",
|
||||||
"name": "John Doe",
|
"name": "John Doe",
|
||||||
"name_parts": {
|
"name_parts": {
|
||||||
"_scheme": "full",
|
"_scheme": "full",
|
||||||
@@ -118,6 +124,7 @@ Endpoints
|
|||||||
"identifier": "8WSAJCJ",
|
"identifier": "8WSAJCJ",
|
||||||
"external_identifier": null,
|
"external_identifier": null,
|
||||||
"email": "customer@example.org",
|
"email": "customer@example.org",
|
||||||
|
"phone": "+493012345678",
|
||||||
"name": "John Doe",
|
"name": "John Doe",
|
||||||
"name_parts": {
|
"name_parts": {
|
||||||
"_scheme": "full",
|
"_scheme": "full",
|
||||||
@@ -155,6 +162,7 @@ Endpoints
|
|||||||
|
|
||||||
{
|
{
|
||||||
"email": "test@example.org",
|
"email": "test@example.org",
|
||||||
|
"phone": "+493012345678",
|
||||||
"password": "verysecret",
|
"password": "verysecret",
|
||||||
"send_email": true
|
"send_email": true
|
||||||
}
|
}
|
||||||
@@ -171,6 +179,7 @@ Endpoints
|
|||||||
"identifier": "8WSAJCJ",
|
"identifier": "8WSAJCJ",
|
||||||
"external_identifier": null,
|
"external_identifier": null,
|
||||||
"email": "test@example.org",
|
"email": "test@example.org",
|
||||||
|
"phone": "+493012345678",
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,6 +224,7 @@ Endpoints
|
|||||||
"identifier": "8WSAJCJ",
|
"identifier": "8WSAJCJ",
|
||||||
"external_identifier": null,
|
"external_identifier": null,
|
||||||
"email": "test@example.org",
|
"email": "test@example.org",
|
||||||
|
"phone": "+493012345678",
|
||||||
…
|
…
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,6 +259,7 @@ Endpoints
|
|||||||
"identifier": "8WSAJCJ",
|
"identifier": "8WSAJCJ",
|
||||||
"external_identifier": null,
|
"external_identifier": null,
|
||||||
"email": null,
|
"email": null,
|
||||||
|
"phone": null,
|
||||||
…
|
…
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,12 @@ id integer Internal ID
|
|||||||
active boolean The discount will be ignored if this is ``false``
|
active boolean The discount will be ignored if this is ``false``
|
||||||
internal_name string A name for the rule used in the backend
|
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
|
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
|
all_sales_channels boolean If ``true`` (default), the discount is available on all sales channels
|
||||||
``"web"`` or ``"resellers"``. Defaults to ``["web"]``.
|
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
|
available_from datetime The first date time at which this discount can be applied
|
||||||
(or ``null``).
|
(or ``null``).
|
||||||
available_until datetime The last date time at which this discount can be applied
|
available_until datetime The last date time at which this discount can be applied
|
||||||
@@ -95,6 +99,8 @@ Endpoints
|
|||||||
"active": true,
|
"active": true,
|
||||||
"internal_name": "3 for 2",
|
"internal_name": "3 for 2",
|
||||||
"position": 1,
|
"position": 1,
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
@@ -151,6 +157,8 @@ Endpoints
|
|||||||
"active": true,
|
"active": true,
|
||||||
"internal_name": "3 for 2",
|
"internal_name": "3 for 2",
|
||||||
"position": 1,
|
"position": 1,
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
@@ -193,6 +201,8 @@ Endpoints
|
|||||||
"active": true,
|
"active": true,
|
||||||
"internal_name": "3 for 2",
|
"internal_name": "3 for 2",
|
||||||
"position": 1,
|
"position": 1,
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
@@ -224,6 +234,8 @@ Endpoints
|
|||||||
"active": true,
|
"active": true,
|
||||||
"internal_name": "3 for 2",
|
"internal_name": "3 for 2",
|
||||||
"position": 1,
|
"position": 1,
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
@@ -284,6 +296,8 @@ Endpoints
|
|||||||
"active": false,
|
"active": false,
|
||||||
"internal_name": "3 for 2",
|
"internal_name": "3 for 2",
|
||||||
"position": 1,
|
"position": 1,
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_until": 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.
|
valid_keys object Cryptographic keys for non-default signature schemes.
|
||||||
For performance reason, value is omitted in lists and
|
For performance reason, value is omitted in lists and
|
||||||
only contained in detail views. Value can be cached.
|
only contained in detail views. Value can be cached.
|
||||||
sales_channels list A list of sales channels this event is available for
|
all_sales_channels boolean If ``true`` (default), the event is available on all sales channels.
|
||||||
sale on.
|
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).
|
public_url string The public, customer-facing URL of the event (read-only).
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
@@ -131,11 +134,13 @@ Endpoints
|
|||||||
"pretix.plugins.paypal",
|
"pretix.plugins.paypal",
|
||||||
"pretix.plugins.ticketoutputpdf"
|
"pretix.plugins.ticketoutputpdf"
|
||||||
],
|
],
|
||||||
"sales_channels": [
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": [
|
||||||
"web",
|
"web",
|
||||||
"pretixpos",
|
"pretixpos",
|
||||||
"resellers"
|
"resellers"
|
||||||
],
|
],
|
||||||
|
"sales_channels": [],
|
||||||
"public_url": "https://pretix.eu/bigevents/sampleconf/"
|
"public_url": "https://pretix.eu/bigevents/sampleconf/"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -225,6 +230,8 @@ Endpoints
|
|||||||
"LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUNvd0JRWURLMlZ3QXlFQTdBRDcvdkZBMzNFc1k0ejJQSHI3aVpQc1o4bjVkaDBhalA4Z3l6Tm1tSXM9Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo="
|
"LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUNvd0JRWURLMlZ3QXlFQTdBRDcvdkZBMzNFc1k0ejJQSHI3aVpQc1o4bjVkaDBhalA4Z3l6Tm1tSXM9Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo="
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"all_sales_channels": true,
|
||||||
|
"limit_sales_channels": [],
|
||||||
"sales_channels": [
|
"sales_channels": [
|
||||||
"web",
|
"web",
|
||||||
"pretixpos",
|
"pretixpos",
|
||||||
@@ -282,11 +289,8 @@ Endpoints
|
|||||||
"pretix.plugins.stripe",
|
"pretix.plugins.stripe",
|
||||||
"pretix.plugins.paypal"
|
"pretix.plugins.paypal"
|
||||||
],
|
],
|
||||||
"sales_channels": [
|
"all_sales_channels": true,
|
||||||
"web",
|
"limit_sales_channels": []
|
||||||
"pretixpos",
|
|
||||||
"resellers"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
**Example response**:
|
**Example response**:
|
||||||
@@ -322,6 +326,8 @@ Endpoints
|
|||||||
"pretix.plugins.stripe",
|
"pretix.plugins.stripe",
|
||||||
"pretix.plugins.paypal"
|
"pretix.plugins.paypal"
|
||||||
],
|
],
|
||||||
|
"all_sales_channels": true,
|
||||||
|
"limit_sales_channels": [],
|
||||||
"sales_channels": [
|
"sales_channels": [
|
||||||
"web",
|
"web",
|
||||||
"pretixpos",
|
"pretixpos",
|
||||||
@@ -387,11 +393,8 @@ Endpoints
|
|||||||
"pretix.plugins.stripe",
|
"pretix.plugins.stripe",
|
||||||
"pretix.plugins.paypal"
|
"pretix.plugins.paypal"
|
||||||
],
|
],
|
||||||
"sales_channels": [
|
"all_sales_channels": true,
|
||||||
"web",
|
"limit_sales_channels": []
|
||||||
"pretixpos",
|
|
||||||
"resellers"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
**Example response**:
|
**Example response**:
|
||||||
@@ -427,6 +430,8 @@ Endpoints
|
|||||||
"pretix.plugins.stripe",
|
"pretix.plugins.stripe",
|
||||||
"pretix.plugins.paypal"
|
"pretix.plugins.paypal"
|
||||||
],
|
],
|
||||||
|
"all_sales_channels": true,
|
||||||
|
"limit_sales_channels": [],
|
||||||
"sales_channels": [
|
"sales_channels": [
|
||||||
"web",
|
"web",
|
||||||
"pretixpos",
|
"pretixpos",
|
||||||
@@ -502,6 +507,8 @@ Endpoints
|
|||||||
"pretix.plugins.paypal",
|
"pretix.plugins.paypal",
|
||||||
"pretix.plugins.pretixdroid"
|
"pretix.plugins.pretixdroid"
|
||||||
],
|
],
|
||||||
|
"all_sales_channels": true,
|
||||||
|
"limit_sales_channels": [],
|
||||||
"sales_channels": [
|
"sales_channels": [
|
||||||
"web",
|
"web",
|
||||||
"pretixpos",
|
"pretixpos",
|
||||||
|
|||||||
@@ -96,6 +96,8 @@ Endpoints
|
|||||||
|
|
||||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||||
:query string secret: Only show gift cards with the given secret.
|
:query 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 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 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.
|
: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
|
checkinlists
|
||||||
waitinglist
|
waitinglist
|
||||||
customers
|
customers
|
||||||
|
saleschannels
|
||||||
membershiptypes
|
membershiptypes
|
||||||
memberships
|
memberships
|
||||||
giftcards
|
giftcards
|
||||||
@@ -43,5 +44,7 @@ at :ref:`plugin-docs`.
|
|||||||
scheduled_exports
|
scheduled_exports
|
||||||
shredders
|
shredders
|
||||||
sendmail_rules
|
sendmail_rules
|
||||||
|
auto_checkin_rules
|
||||||
billing_invoices
|
billing_invoices
|
||||||
billing_var
|
billing_var
|
||||||
|
seats
|
||||||
|
|||||||
@@ -217,6 +217,9 @@ List of all invoices
|
|||||||
:query boolean is_cancellation: If set to ``true`` or ``false``, only invoices with this value for the field
|
:query boolean is_cancellation: If set to ``true`` or ``false``, only invoices with this value for the field
|
||||||
``is_cancellation`` will be returned.
|
``is_cancellation`` will be returned.
|
||||||
:query string order: If set, only invoices belonging to the order with the given order code will be returned.
|
:query string order: If set, only invoices belonging to the order with the given order code will be returned.
|
||||||
|
This parameter may be given multiple times. In this case, all invoices matching one of the inputs will be returned.
|
||||||
|
:query string number: If set, only invoices with the given invoice number will be returned.
|
||||||
|
This parameter may be given multiple times. In this case, all invoices matching one of the inputs will be returned.
|
||||||
:query string refers: If set, only invoices referring to the given invoice will be returned.
|
:query string refers: If set, only invoices referring to the given invoice will be returned.
|
||||||
:query string locale: If set, only invoices with the given locale will be returned.
|
:query string locale: If set, only invoices with the given locale will be returned.
|
||||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``date`` and
|
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``date`` and
|
||||||
|
|||||||
@@ -38,11 +38,14 @@ require_membership boolean If ``true``, bo
|
|||||||
require_membership_hidden boolean If ``true`` and ``require_membership`` is set, this variation will
|
require_membership_hidden boolean If ``true`` and ``require_membership`` is set, this variation will
|
||||||
be hidden from users without a valid membership.
|
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``
|
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
|
all_sales_channels boolean If ``true`` (default), the variation is available on all sales channels.
|
||||||
``"web"`` or ``"resellers"``. Defaults to all existing 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
|
The item-level list takes precedence, i.e. a sales
|
||||||
channel needs to be on both lists for the item to be
|
channel needs to be on both lists for the variation to be
|
||||||
available.
|
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
|
available_from datetime The first date time at which this variation can be bought
|
||||||
(or ``null``).
|
(or ``null``).
|
||||||
available_from_mode string If ``hide`` (the default), this variation is hidden in the shop
|
available_from_mode string If ``hide`` (the default), this variation is hidden in the shop
|
||||||
@@ -111,6 +114,8 @@ Endpoints
|
|||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_hidden": false,
|
"require_membership_hidden": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
@@ -139,6 +144,8 @@ Endpoints
|
|||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_hidden": false,
|
"require_membership_hidden": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
@@ -157,6 +164,7 @@ Endpoints
|
|||||||
}
|
}
|
||||||
|
|
||||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:query string 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
|
:query boolean active: If set to ``true`` or ``false``, only items with this value for the field ``active`` will be
|
||||||
returned.
|
returned.
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -202,6 +210,8 @@ Endpoints
|
|||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_hidden": false,
|
"require_membership_hidden": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
@@ -244,7 +254,8 @@ Endpoints
|
|||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_hidden": false,
|
"require_membership_hidden": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
@@ -277,6 +288,8 @@ Endpoints
|
|||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_hidden": false,
|
"require_membership_hidden": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
@@ -341,6 +354,8 @@ Endpoints
|
|||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_hidden": false,
|
"require_membership_hidden": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
|
|||||||
@@ -46,8 +46,11 @@ personalized boolean ``true`` for
|
|||||||
position integer An integer, used for sorting
|
position integer An integer, used for sorting
|
||||||
picture file A product picture to be displayed in the shop
|
picture file A product picture to be displayed in the shop
|
||||||
(can be ``null``).
|
(can be ``null``).
|
||||||
sales_channels list of strings Sales channels this product is available on, such as
|
all_sales_channels boolean If ``true`` (default), the item is available on all sales channels.
|
||||||
``"web"`` or ``"resellers"``. Defaults to ``["web"]``.
|
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
|
available_from datetime The first date time at which this item can be bought
|
||||||
(or ``null``).
|
(or ``null``).
|
||||||
available_from_mode string If ``hide`` (the default), this item is hidden in the shop
|
available_from_mode string If ``hide`` (the default), this item is hidden in the shop
|
||||||
@@ -157,11 +160,14 @@ variations list of objects A list with o
|
|||||||
be hidden from users without a valid membership.
|
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``
|
├ require_membership_types list of integers Internal IDs of membership types valid if ``require_membership`` is ``true``
|
||||||
Markdown syntax or can be ``null``.
|
Markdown syntax or can be ``null``.
|
||||||
├ sales_channels list of strings Sales channels this variation is available on, such as
|
├ all_sales_channels boolean If ``true`` (default), the variation is available on all sales channels.
|
||||||
``"web"`` or ``"resellers"``. Defaults to all existing 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
|
The item-level list takes precedence, i.e. a sales
|
||||||
channel needs to be on both lists for the item to be
|
channel needs to be on both lists for the variation to be
|
||||||
available.
|
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
|
├ available_from datetime The first date time at which this variation can be bought
|
||||||
(or ``null``).
|
(or ``null``).
|
||||||
├ available_from_mode string If ``hide`` (the default), this variation is hidden in the shop
|
├ available_from_mode string If ``hide`` (the default), this variation is hidden in the shop
|
||||||
@@ -276,6 +282,8 @@ Endpoints
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "Standard ticket"},
|
"name": {"en": "Standard ticket"},
|
||||||
"internal_name": "",
|
"internal_name": "",
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"default_price": "23.00",
|
"default_price": "23.00",
|
||||||
"original_price": null,
|
"original_price": null,
|
||||||
@@ -340,6 +348,8 @@ Endpoints
|
|||||||
"require_approval": false,
|
"require_approval": false,
|
||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
@@ -362,6 +372,8 @@ Endpoints
|
|||||||
"require_approval": false,
|
"require_approval": false,
|
||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
@@ -380,6 +392,7 @@ Endpoints
|
|||||||
}
|
}
|
||||||
|
|
||||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:query string 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
|
:query boolean active: If set to ``true`` or ``false``, only items with this value for the field ``active`` will be
|
||||||
returned.
|
returned.
|
||||||
:query integer category: If set to the ID of a category, only items within that category will be returned.
|
:query integer category: If set to the ID of a category, only items within that category will be returned.
|
||||||
@@ -420,6 +433,8 @@ Endpoints
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "Standard ticket"},
|
"name": {"en": "Standard ticket"},
|
||||||
"internal_name": "",
|
"internal_name": "",
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"default_price": "23.00",
|
"default_price": "23.00",
|
||||||
"original_price": null,
|
"original_price": null,
|
||||||
@@ -485,6 +500,8 @@ Endpoints
|
|||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"description": null,
|
"description": null,
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
@@ -506,6 +523,8 @@ Endpoints
|
|||||||
"require_approval": false,
|
"require_approval": false,
|
||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
@@ -545,7 +564,8 @@ Endpoints
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "Standard ticket"},
|
"name": {"en": "Standard ticket"},
|
||||||
"internal_name": "",
|
"internal_name": "",
|
||||||
"sales_channels": ["web"],
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"default_price": "23.00",
|
"default_price": "23.00",
|
||||||
"original_price": null,
|
"original_price": null,
|
||||||
"category": null,
|
"category": null,
|
||||||
@@ -608,7 +628,8 @@ Endpoints
|
|||||||
"require_approval": false,
|
"require_approval": false,
|
||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
@@ -630,7 +651,8 @@ Endpoints
|
|||||||
"require_approval": false,
|
"require_approval": false,
|
||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
@@ -657,6 +679,8 @@ Endpoints
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "Standard ticket"},
|
"name": {"en": "Standard ticket"},
|
||||||
"internal_name": "",
|
"internal_name": "",
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"default_price": "23.00",
|
"default_price": "23.00",
|
||||||
"original_price": null,
|
"original_price": null,
|
||||||
@@ -721,6 +745,8 @@ Endpoints
|
|||||||
"require_approval": false,
|
"require_approval": false,
|
||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
@@ -743,6 +769,8 @@ Endpoints
|
|||||||
"require_approval": false,
|
"require_approval": false,
|
||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
@@ -801,6 +829,8 @@ Endpoints
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "Ticket"},
|
"name": {"en": "Ticket"},
|
||||||
"internal_name": "",
|
"internal_name": "",
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"default_price": "25.00",
|
"default_price": "25.00",
|
||||||
"original_price": null,
|
"original_price": null,
|
||||||
@@ -865,6 +895,8 @@ Endpoints
|
|||||||
"require_approval": false,
|
"require_approval": false,
|
||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
@@ -887,6 +919,8 @@ Endpoints
|
|||||||
"require_approval": false,
|
"require_approval": false,
|
||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ payment_date date **DEPRECATED AN
|
|||||||
payment_provider string **DEPRECATED AND INACCURATE** Payment provider used for this order
|
payment_provider string **DEPRECATED AND INACCURATE** Payment provider used for this order
|
||||||
total money (string) Total value of this order
|
total money (string) Total value of this order
|
||||||
comment string Internal comment on this order
|
comment string Internal comment on this order
|
||||||
|
api_meta object Meta data for that order. Only available through API, no guarantees
|
||||||
|
on the content structure. You can use this to save references to your system.
|
||||||
custom_followup_at date Internal date for a custom follow-up action
|
custom_followup_at date Internal date for a custom follow-up action
|
||||||
checkin_attention boolean If ``true``, the check-in app should show a warning
|
checkin_attention boolean If ``true``, the check-in app should show a warning
|
||||||
that this ticket requires special attention if a ticket
|
that this ticket requires special attention if a ticket
|
||||||
@@ -201,7 +203,8 @@ checkins list of objects List of **succe
|
|||||||
├ datetime datetime Time of check-in
|
├ datetime datetime Time of check-in
|
||||||
├ type string Type of scan (defaults to ``entry``)
|
├ type string Type of scan (defaults to ``entry``)
|
||||||
├ gate integer Internal ID of the gate. Can be ``null``.
|
├ gate integer Internal ID of the gate. Can be ``null``.
|
||||||
├ device integer Internal ID of the device. Can be ``null``.
|
├ device integer Internal ID of the device. Can be ``null``. **Deprecated**, since this ID is not otherwise used in the API and is therefore not very useful.
|
||||||
|
├ device_id integer Attribute ``device_id`` of the device. Can be ``null``.
|
||||||
└ auto_checked_in boolean Indicates if this check-in been performed automatically by the system
|
└ auto_checked_in boolean Indicates if this check-in been performed automatically by the system
|
||||||
downloads list of objects List of ticket download options
|
downloads list of objects List of ticket download options
|
||||||
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
|
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
|
||||||
@@ -215,6 +218,11 @@ answers list of objects Answers to user
|
|||||||
seat objects The assigned seat. Can be ``null``.
|
seat objects The assigned seat. Can be ``null``.
|
||||||
├ id integer Internal ID of the seat instance
|
├ id integer Internal ID of the seat instance
|
||||||
├ name string Human-readable seat name
|
├ 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
|
└ seat_guid string Identifier of the seat within the seating plan
|
||||||
pdf_data object Data object required for ticket PDF generation. By default,
|
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
|
this field is missing. It will be added only if you add the
|
||||||
@@ -455,10 +463,13 @@ List of all orders
|
|||||||
:query datetime modified_since: Only return orders that have changed since the given date. Be careful: We only
|
: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
|
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.
|
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 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_after: Only return orders that contain a ticket for a subevent taking place after the given date. This is an exclusive after, and it considers the **end** of the subevent (or its start, if the end is not set).
|
||||||
:query datetime subevent_before: Only return orders that contain a ticket for a subevent taking place after the given date. This is an exclusive before, and it considers the **start** of the subevent.
|
:query datetime subevent_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 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.
|
: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
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -554,6 +565,7 @@ Fetching individual orders
|
|||||||
"fees": [],
|
"fees": [],
|
||||||
"total": "23.00",
|
"total": "23.00",
|
||||||
"comment": "",
|
"comment": "",
|
||||||
|
"api_meta": {},
|
||||||
"custom_followup_at": null,
|
"custom_followup_at": null,
|
||||||
"checkin_attention": false,
|
"checkin_attention": false,
|
||||||
"checkin_text": null,
|
"checkin_text": null,
|
||||||
@@ -734,6 +746,8 @@ Updating order fields
|
|||||||
|
|
||||||
* ``comment``
|
* ``comment``
|
||||||
|
|
||||||
|
* ``api_meta``
|
||||||
|
|
||||||
* ``custom_followup_at``
|
* ``custom_followup_at``
|
||||||
|
|
||||||
* ``invoice_address`` (you always need to supply the full object, or ``null`` to delete the current address)
|
* ``invoice_address`` (you always need to supply the full object, or ``null`` to delete the current address)
|
||||||
|
|||||||
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",
|
||||||
|
"label": {
|
||||||
|
"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",
|
||||||
|
"label": {
|
||||||
|
"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",
|
||||||
|
"label": {
|
||||||
|
"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",
|
||||||
|
"label": {
|
||||||
|
"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",
|
||||||
|
"label": {
|
||||||
|
"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.
|
||||||
262
doc/api/resources/seats.rst
Normal file
262
doc/api/resources/seats.rst
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
.. _`rest-reusablemedia`:
|
||||||
|
|
||||||
|
Seats
|
||||||
|
=====
|
||||||
|
|
||||||
|
The seat resource represents the seats in a seating plan in a specific event or subevent.
|
||||||
|
|
||||||
|
Resource description
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
The seat resource contains the following public fields:
|
||||||
|
|
||||||
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
Field Type Description
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
id integer Internal ID of this seat
|
||||||
|
subevent integer Internal ID of the subevent this seat belongs to
|
||||||
|
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
|
||||||
|
product integer Internal ID of the product that is mapped to this seat
|
||||||
|
blocked boolean Whether this seat is blocked manually.
|
||||||
|
orderposition integer / object Internal ID of an order position reserving this seat.
|
||||||
|
cartposition integer / object Internal ID of a cart position reserving this seat.
|
||||||
|
voucher integer / object Internal ID of a voucher reserving this seat.
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
Endpoints
|
||||||
|
---------
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/seats/
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/subevents/(subevent_id)/seats/
|
||||||
|
|
||||||
|
Returns a list of all seats in the specified event or subevent. Depending on whether the event has subevents, the
|
||||||
|
according endpoint has to be used.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/seats/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"count": 500,
|
||||||
|
"next": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/seats/?page=2",
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1633,
|
||||||
|
"subevent": null,
|
||||||
|
"zone_name": "Ground floor",
|
||||||
|
"row_name": "1",
|
||||||
|
"row_label": null,
|
||||||
|
"seat_number": "1",
|
||||||
|
"seat_label": null,
|
||||||
|
"seat_guid": "b9746230-6f31-4f41-bbc9-d6b60bdb3342",
|
||||||
|
"product": 104,
|
||||||
|
"blocked": false,
|
||||||
|
"orderposition": null,
|
||||||
|
"cartposition": null,
|
||||||
|
"voucher": 51
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1634,
|
||||||
|
"subevent": null,
|
||||||
|
"zone_name": "Ground floor",
|
||||||
|
"row_name": "1",
|
||||||
|
"row_label": null,
|
||||||
|
"seat_number": "2",
|
||||||
|
"seat_label": null,
|
||||||
|
"seat_guid": "1d29fe20-8e1e-4984-b0ee-2773b0d07e07",
|
||||||
|
"product": 104,
|
||||||
|
"blocked": true,
|
||||||
|
"orderposition": 4321,
|
||||||
|
"cartposition": null,
|
||||||
|
"voucher": null
|
||||||
|
},
|
||||||
|
// ...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
:query integer page: The page number in case of a multi-page result set, default is 1.
|
||||||
|
:query string zone_name: Only show seats with the given zone_name.
|
||||||
|
:query string row_name: Only show seats with the given row_name.
|
||||||
|
:query string row_label: Only show seats with the given row_label.
|
||||||
|
:query string seat_number: Only show seats with the given seat_number.
|
||||||
|
:query string seat_label: Only show seats with the given seat_label.
|
||||||
|
:query string seat_guid: Only show seats with the given seat_guid.
|
||||||
|
:query boolean blocked: Only show seats with the given blocked status.
|
||||||
|
:query boolean is_available: Only show seats that are (not) currently available.
|
||||||
|
:query string expand: If you pass ``"orderposition"``, ``"cartposition"``, or ``"voucher"``, the respective field will be
|
||||||
|
shown as a nested value instead of just an ID. This requires permission to access that object.
|
||||||
|
The nested objects are identical to the respective resources, except that order positions
|
||||||
|
will have an attribute of the format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make
|
||||||
|
matching easier, and won't include the `seat` attribute, as that would be redundant.
|
||||||
|
The parameter can be given multiple times.
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param event: The ``slug`` field of the event to fetch
|
||||||
|
:param subevent_id: The ``id`` field of the subevent to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||||
|
:statuscode 404: Endpoint without subevent id was used for event with subevents, or vice versa.
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/seats/(id)/
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/subevents/(subevent_id)/seats/(id)/
|
||||||
|
|
||||||
|
Returns information on one seat, identified by its ID.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/seats/1634/?expand=orderposition HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1634,
|
||||||
|
"subevent": null,
|
||||||
|
"zone_name": "Ground floor",
|
||||||
|
"row_name": "1",
|
||||||
|
"row_label": null,
|
||||||
|
"seat_number": "2",
|
||||||
|
"seat_label": null,
|
||||||
|
"seat_guid": "1d29fe20-8e1e-4984-b0ee-2773b0d07e07",
|
||||||
|
"product": 104,
|
||||||
|
"blocked": true,
|
||||||
|
"orderposition": {
|
||||||
|
"id": 134,
|
||||||
|
"order": {
|
||||||
|
"code": "U0HW7",
|
||||||
|
"event": "sampleconf"
|
||||||
|
},
|
||||||
|
"positionid": 1,
|
||||||
|
"item": 104,
|
||||||
|
"variation": 59,
|
||||||
|
"price": "60.00",
|
||||||
|
"attendee_name": "",
|
||||||
|
"attendee_name_parts": {
|
||||||
|
"_scheme": "given_family"
|
||||||
|
},
|
||||||
|
"company": null,
|
||||||
|
"street": null,
|
||||||
|
"zipcode": null,
|
||||||
|
"city": null,
|
||||||
|
"country": null,
|
||||||
|
"state": null,
|
||||||
|
"discount": null,
|
||||||
|
"attendee_email": null,
|
||||||
|
"voucher": null,
|
||||||
|
"tax_rate": "0.00",
|
||||||
|
"tax_value": "0.00",
|
||||||
|
"secret": "4rfgp263jduratnsvwvy6cc6r6wnptbj",
|
||||||
|
"addon_to": null,
|
||||||
|
"subevent": null,
|
||||||
|
"checkins": [],
|
||||||
|
"downloads": [],
|
||||||
|
"answers": [],
|
||||||
|
"tax_rule": null,
|
||||||
|
"pseudonymization_id": "ZSNYSG3URZ",
|
||||||
|
"canceled": false,
|
||||||
|
"valid_from": null,
|
||||||
|
"valid_until": null,
|
||||||
|
"blocked": null,
|
||||||
|
"voucher_budget_use": null
|
||||||
|
},
|
||||||
|
"cartposition": null,
|
||||||
|
"voucher": null
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param event: The ``slug`` field of the event to fetch
|
||||||
|
:param subevent_id: The ``id`` field of the subevent to fetch
|
||||||
|
:param id: The ``id`` field of the seat to fetch
|
||||||
|
:query string expand: If you pass ``"orderposition"``, ``"cartposition"``, or ``"voucher"``, the respective field will be
|
||||||
|
shown as a nested value instead of just an ID. This requires permission to access that object.
|
||||||
|
The nested objects are identical to the respective resources, except that order positions
|
||||||
|
will have an attribute of the format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make
|
||||||
|
matching easier, and won't include the `seat` attribute, as that would be redundant.
|
||||||
|
The parameter can be given multiple times.
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||||
|
:statuscode 404: Seat does not exist; or the endpoint without subevent id was used for event with subevents, or vice versa.
|
||||||
|
|
||||||
|
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/seats/(id)/
|
||||||
|
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/seats/(id)/
|
||||||
|
|
||||||
|
Update a seat.
|
||||||
|
|
||||||
|
You can only change the ``blocked`` field.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
PATCH /api/v1/organizers/bigevents/events/sampleconf/seats/1636/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"blocked": true
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1636,
|
||||||
|
"subevent": null,
|
||||||
|
"zone_name": "Ground floor",
|
||||||
|
"row_name": "1",
|
||||||
|
"row_label": null,
|
||||||
|
"seat_number": "4",
|
||||||
|
"seat_label": null,
|
||||||
|
"seat_guid": "6c0e29e5-05d6-421f-99f3-afd01478ecad",
|
||||||
|
"product": 104,
|
||||||
|
"blocked": true,
|
||||||
|
"orderposition": null,
|
||||||
|
"cartposition": null,
|
||||||
|
"voucher": null
|
||||||
|
},
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param event: The ``slug`` field of the event to modify
|
||||||
|
:param subevent_id: The ``id`` field of the subevent to modify
|
||||||
|
:param id: The ``id`` field of the seat to modify
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 400: The seat could not be modified due to invalid submitted data
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to change this resource.
|
||||||
|
:statuscode 404: Seat does not exist; or the endpoint without subevent id was used for event with subevents, or vice versa.
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
Scheduled email rules
|
Scheduled email rules
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
|
This feature requires the bundled ``pretix.plugins.sendmail`` plugin to be active for the event in order to work properly.
|
||||||
|
|
||||||
Resource description
|
Resource description
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
@@ -48,6 +50,7 @@ send_to string Can be ``"order
|
|||||||
or ``"both"``.
|
or ``"both"``.
|
||||||
date. Otherwise it is relative to the event start date.
|
date. Otherwise it is relative to the event start date.
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
.. versionchanged:: 2023.7
|
.. versionchanged:: 2023.7
|
||||||
|
|
||||||
The ``include_pending`` field has been deprecated.
|
The ``include_pending`` field has been deprecated.
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ Endpoints
|
|||||||
}
|
}
|
||||||
|
|
||||||
:query page: The page number in case of a multi-page result set, default is 1
|
:query page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:query is_public: If set to ``true``/``false``, only subevents with a matching value of ``is_public`` are returned.
|
||||||
:query active: If set to ``true``/``false``, only events with a matching value of ``active`` are returned.
|
:query active: If set to ``true``/``false``, only events with a matching value of ``active`` are returned.
|
||||||
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned.
|
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned.
|
||||||
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned.
|
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned.
|
||||||
@@ -467,6 +468,7 @@ Endpoints
|
|||||||
}
|
}
|
||||||
|
|
||||||
:query page: The page number in case of a multi-page result set, default is 1
|
:query page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:query is_public: If set to ``true``/``false``, only subevents with a matching value of ``is_public`` are returned.
|
||||||
:query active: If set to ``true``/``false``, only events with a matching value of ``active`` are returned.
|
:query active: If set to ``true``/``false``, only events with a matching value of ``active`` are returned.
|
||||||
:query event__live: If set to ``true``/``false``, only events with a matching value of ``live`` on the parent event are returned.
|
:query event__live: If set to ``true``/``false``, only events with a matching value of ``live`` on the parent event are returned.
|
||||||
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned.
|
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned.
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ internal_name string An optional nam
|
|||||||
rate decimal (string) Tax rate in percent
|
rate decimal (string) Tax rate in percent
|
||||||
price_includes_tax boolean If ``true`` (default), tax is assumed to be included in
|
price_includes_tax boolean If ``true`` (default), tax is assumed to be included in
|
||||||
the specified product price
|
the specified product price
|
||||||
eu_reverse_charge boolean If ``true``, EU reverse charge rules are applied. Will
|
eu_reverse_charge boolean **DEPRECATED**. If ``true``, EU reverse charge rules
|
||||||
be ignored if custom rules are set.
|
are applied. Will be ignored if custom rules are set.
|
||||||
|
Use custom rules instead.
|
||||||
home_country string Merchant country (required for reverse charge), can be
|
home_country string Merchant country (required for reverse charge), can be
|
||||||
``null`` or empty string
|
``null`` or empty string
|
||||||
keep_gross_if_rate_changes boolean If ``true``, changes of the tax rate based on custom
|
keep_gross_if_rate_changes boolean If ``true``, changes of the tax rate based on custom
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ id integer Internal ID of
|
|||||||
name string Team name
|
name string Team name
|
||||||
all_events boolean Whether this team has access to all events
|
all_events boolean Whether this team has access to all events
|
||||||
limit_events list List of event slugs this team has access to
|
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_create_events boolean
|
||||||
can_change_teams boolean
|
can_change_teams boolean
|
||||||
can_change_organizer_settings boolean
|
can_change_organizer_settings boolean
|
||||||
@@ -122,6 +124,7 @@ Team endpoints
|
|||||||
"name": "Admin team",
|
"name": "Admin team",
|
||||||
"all_events": true,
|
"all_events": true,
|
||||||
"limit_events": [],
|
"limit_events": [],
|
||||||
|
"require_2fa": true,
|
||||||
"can_create_events": true,
|
"can_create_events": true,
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
@@ -159,6 +162,7 @@ Team endpoints
|
|||||||
"name": "Admin team",
|
"name": "Admin team",
|
||||||
"all_events": true,
|
"all_events": true,
|
||||||
"limit_events": [],
|
"limit_events": [],
|
||||||
|
"require_2fa": true,
|
||||||
"can_create_events": true,
|
"can_create_events": true,
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
@@ -186,6 +190,7 @@ Team endpoints
|
|||||||
"name": "Admin team",
|
"name": "Admin team",
|
||||||
"all_events": true,
|
"all_events": true,
|
||||||
"limit_events": [],
|
"limit_events": [],
|
||||||
|
"require_2fa": true,
|
||||||
"can_create_events": true,
|
"can_create_events": true,
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
@@ -203,6 +208,7 @@ Team endpoints
|
|||||||
"name": "Admin team",
|
"name": "Admin team",
|
||||||
"all_events": true,
|
"all_events": true,
|
||||||
"limit_events": [],
|
"limit_events": [],
|
||||||
|
"require_2fa": true,
|
||||||
"can_create_events": true,
|
"can_create_events": true,
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
@@ -246,6 +252,7 @@ Team endpoints
|
|||||||
"name": "Admin team",
|
"name": "Admin team",
|
||||||
"all_events": true,
|
"all_events": true,
|
||||||
"limit_events": [],
|
"limit_events": [],
|
||||||
|
"require_2fa": true,
|
||||||
"can_create_events": true,
|
"can_create_events": true,
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ The following values for ``action_types`` are valid with pretix core:
|
|||||||
* ``pretix.event.order.modified``
|
* ``pretix.event.order.modified``
|
||||||
* ``pretix.event.order.contact.changed``
|
* ``pretix.event.order.contact.changed``
|
||||||
* ``pretix.event.order.changed.*``
|
* ``pretix.event.order.changed.*``
|
||||||
|
* ``pretix.event.order.deleted`` (can only occur for test mode orders)
|
||||||
* ``pretix.event.order.refund.created``
|
* ``pretix.event.order.refund.created``
|
||||||
* ``pretix.event.order.refund.created.externally``
|
* ``pretix.event.order.refund.created.externally``
|
||||||
* ``pretix.event.order.refund.requested``
|
* ``pretix.event.order.refund.requested``
|
||||||
@@ -115,6 +116,7 @@ Endpoints
|
|||||||
}
|
}
|
||||||
|
|
||||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:query boolean enabled: Only show webhooks that are or are not enabled
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ Core
|
|||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types, notification,
|
: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,
|
item_copy_data, register_sales_channel_types, register_global_settings, quota_availability, global_email_filter,
|
||||||
register_ticket_secret_generators, gift_card_transaction_display,
|
register_ticket_secret_generators, gift_card_transaction_display,
|
||||||
register_text_placeholders, register_mail_placeholders
|
register_text_placeholders, register_mail_placeholders, device_info_updated
|
||||||
|
|
||||||
Order events
|
Order events
|
||||||
""""""""""""
|
""""""""""""
|
||||||
@@ -35,11 +35,11 @@ Frontend
|
|||||||
--------
|
--------
|
||||||
|
|
||||||
.. automodule:: pretix.presale.signals
|
.. 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
|
.. automodule:: pretix.presale.signals
|
||||||
:members: order_info, order_info_top, order_meta_from_request
|
:members: order_info, order_info_top, order_meta_from_request, order_api_meta_from_request
|
||||||
|
|
||||||
Request flow
|
Request flow
|
||||||
""""""""""""
|
""""""""""""
|
||||||
|
|||||||
@@ -3,11 +3,12 @@
|
|||||||
|
|
||||||
.. _`importcol`:
|
.. _`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
|
It's possible through the backend to import objects into pretix, for example orders from a legacy ticketing system. If
|
||||||
plugins defines additional data structures around orders, it might be useful to make it possible to import them as well.
|
your plugin defines additional data structures around those objects, it might be useful to make it possible to import
|
||||||
|
them as well.
|
||||||
|
|
||||||
Import process
|
Import process
|
||||||
--------------
|
--------------
|
||||||
@@ -40,7 +41,7 @@ Column registration
|
|||||||
|
|
||||||
The import API does not make a lot of usage from signals, however, it
|
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
|
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:
|
that we'll provide in this plugin:
|
||||||
|
|
||||||
.. sourcecode:: python
|
.. sourcecode:: python
|
||||||
@@ -56,10 +57,16 @@ that we'll provide in this plugin:
|
|||||||
EmailColumn(sender),
|
EmailColumn(sender),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
Similar signals exist for other objects:
|
||||||
|
|
||||||
|
.. automodule:: pretix.base.signals
|
||||||
|
:members: voucher_import_columns
|
||||||
|
|
||||||
|
|
||||||
The column class API
|
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``.
|
The central object of each import extension is the subclass of ``ImportColumn``.
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,8 @@ convenient to you:
|
|||||||
|
|
||||||
.. automethod:: _register_fonts
|
.. automethod:: _register_fonts
|
||||||
|
|
||||||
|
.. automethod:: _register_event_fonts
|
||||||
|
|
||||||
.. automethod:: _on_first_page
|
.. automethod:: _on_first_page
|
||||||
|
|
||||||
.. automethod:: _on_other_page
|
.. automethod:: _on_other_page
|
||||||
|
|||||||
@@ -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.
|
create and manage their events, items, orders and tickets.
|
||||||
|
|
||||||
**presale**
|
**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.
|
end user. Also called "frontend" in parts of this documentation.
|
||||||
|
|
||||||
**api**
|
**api**
|
||||||
|
|||||||
@@ -19,3 +19,4 @@ Contents:
|
|||||||
permissions
|
permissions
|
||||||
logging
|
logging
|
||||||
locking
|
locking
|
||||||
|
timemachine
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ includes serializers for serializing the following types:
|
|||||||
* Built-in types: ``int``, ``float``, ``decimal.Decimal``, ``dict``, ``list``, ``bool``
|
* Built-in types: ``int``, ``float``, ``decimal.Decimal``, ``dict``, ``list``, ``bool``
|
||||||
* ``datetime.date``, ``datetime.datetime``, ``datetime.time``
|
* ``datetime.date``, ``datetime.datetime``, ``datetime.time``
|
||||||
* ``LazyI18nString``
|
* ``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
|
* References to model instances
|
||||||
|
|
||||||
In code, we recommend to always use the ``.get()`` method on the settings object to access a value, but for
|
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."),
|
"preserve his reservation."),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
.. _settings-defaults-in-plugins:
|
||||||
|
|
||||||
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
|
.. _django-hierarkey: https://github.com/raphaelm/django-hierarkey
|
||||||
.. _documentation: https://django-hierarkey.readthedocs.io/en/latest/
|
.. _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="{% eventurl request.event "presale:event.checkout" step="payment" %}">Pay</a>
|
||||||
<a href="{% abseventurl 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
|
Implementation details
|
||||||
----------------------
|
----------------------
|
||||||
|
|||||||
@@ -136,9 +136,7 @@ It is a good idea to put this command into your git hook ``.git/hooks/pre-commit
|
|||||||
for example, to check for any errors in any staged files when committing::
|
for example, to check for any errors in any staged files when committing::
|
||||||
|
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
cd $GIT_DIR/../src
|
|
||||||
export GIT_WORK_TREE=../
|
|
||||||
export GIT_DIR=../.git
|
|
||||||
source ../env/bin/activate # Adjust to however you activate your virtual environment
|
source ../env/bin/activate # Adjust to however you activate your virtual environment
|
||||||
for file in $(git diff --cached --name-only | grep -E '\.py$' | grep -Ev "migrations|mt940\.py|pretix/settings\.py|make_testdata\.py|testutils/settings\.py|tests/settings\.py|pretix/base/models/__init__\.py|.*_pb2\.py")
|
for file in $(git diff --cached --name-only | grep -E '\.py$' | grep -Ev "migrations|mt940\.py|pretix/settings\.py|make_testdata\.py|testutils/settings\.py|tests/settings\.py|pretix/base/models/__init__\.py|.*_pb2\.py")
|
||||||
do
|
do
|
||||||
@@ -211,5 +209,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
|
Then, go to http://localhost:8081 for a version of the documentation that automatically re-builds
|
||||||
whenever you change a source file.
|
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
|
.. _Django's documentation: https://docs.djangoproject.com/en/1.11/ref/django-admin/#runserver
|
||||||
.. _pretixdroid: https://github.com/pretix/pretixdroid
|
.. _pretixdroid: https://github.com/pretix/pretixdroid
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ pretix/
|
|||||||
Additional code implementing our customized :ref:`URL handling <urlconf>`.
|
Additional code implementing our customized :ref:`URL handling <urlconf>`.
|
||||||
|
|
||||||
static/
|
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
|
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.
|
step as Bootstrap and FontAwesome, so their mixins etc. are fully available.
|
||||||
|
|
||||||
@@ -41,6 +41,6 @@ pretix/
|
|||||||
|
|
||||||
tests/
|
tests/
|
||||||
This is the root directory for all test codes. It includes subdirectories ``api``, ``base``,
|
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
|
of the pretix source code as well as ``testdummy``, which is a pretix plugin used during
|
||||||
testing.
|
testing.
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ allow_voucher_access boolean Enables access
|
|||||||
lead_scanning_scope_by_device string Enables lead scanning to be handled as one lead per attendee
|
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.
|
per scanning device, instead of only per exhibitor.
|
||||||
comment string Internal comment, not shown to 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:
|
You can also access the scanned leads through the API which contains the following public fields:
|
||||||
@@ -119,7 +121,8 @@ Endpoints
|
|||||||
"allow_lead_scanning": true,
|
"allow_lead_scanning": true,
|
||||||
"allow_lead_access": true,
|
"allow_lead_access": true,
|
||||||
"allow_voucher_access": true,
|
"allow_voucher_access": true,
|
||||||
"comment": ""
|
"comment": "",
|
||||||
|
"exhibitor_tags": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -173,7 +176,8 @@ Endpoints
|
|||||||
"allow_lead_scanning": true,
|
"allow_lead_scanning": true,
|
||||||
"allow_lead_access": true,
|
"allow_lead_access": true,
|
||||||
"allow_voucher_access": true,
|
"allow_voucher_access": true,
|
||||||
"comment": ""
|
"comment": "",
|
||||||
|
"exhibitor_tags": []
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -374,7 +378,10 @@ Endpoints
|
|||||||
"allow_lead_scanning": true,
|
"allow_lead_scanning": true,
|
||||||
"allow_lead_access": true,
|
"allow_lead_access": true,
|
||||||
"allow_voucher_access": true,
|
"allow_voucher_access": true,
|
||||||
"comment": ""
|
"comment": "",
|
||||||
|
"exhibitor_tags": [
|
||||||
|
"Gold Sponsor"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
**Example response**:
|
**Example response**:
|
||||||
@@ -407,7 +414,10 @@ Endpoints
|
|||||||
"allow_lead_scanning": true,
|
"allow_lead_scanning": true,
|
||||||
"allow_lead_access": true,
|
"allow_lead_access": true,
|
||||||
"allow_voucher_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
|
:param organizer: The ``slug`` field of the organizer to create new exhibitor for
|
||||||
@@ -468,7 +478,10 @@ Endpoints
|
|||||||
"allow_lead_scanning": true,
|
"allow_lead_scanning": true,
|
||||||
"allow_lead_access": true,
|
"allow_lead_access": true,
|
||||||
"allow_voucher_access": true,
|
"allow_voucher_access": true,
|
||||||
"comment": ""
|
"comment": "",
|
||||||
|
"exhibitor_tags": [
|
||||||
|
"Gold Sponsor"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to modify
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
|||||||
105
doc/plugins/getyourguide.rst
Normal file
105
doc/plugins/getyourguide.rst
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
GetYourGuide
|
||||||
|
============
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The GetYourGuide integration is currently in Beta. Please contact support@pretix.eu to enable the integration
|
||||||
|
for your pretix.eu organizer account.
|
||||||
|
|
||||||
|
Introduction
|
||||||
|
------------
|
||||||
|
Using third party aggregators, such als GetYourGuide, event organizers can sell tickets to their events not only on
|
||||||
|
their own ticket-shop but also on the aggregator's portal. While this service is not for free, it allows event
|
||||||
|
organizers to reacher a larger audience that would otherwise not have found their way into the organizers webshop.
|
||||||
|
|
||||||
|
Using pretix' integration with GetYourGuide, event organizers can profit from an additional sales and revenue channel,
|
||||||
|
while keeping the effort for setting up and maintaining multiple ticket shops to a minimum.
|
||||||
|
|
||||||
|
Preparing your organizer account
|
||||||
|
--------------------------------
|
||||||
|
The first step in enabling the GetYourGuide integration, is to setup a corresponding Sales Channel, which will be used
|
||||||
|
to properly attribute the sales generated. This needs to be done only once per organizer account.
|
||||||
|
|
||||||
|
To do so, log into the pretix backend, select ``Organizers`` from the navigation and then the organizer in question.
|
||||||
|
Extending the ``Settings``-menu, find the ``Sales channels`` configuration and click the ``Add a new channel`` button.
|
||||||
|
|
||||||
|
On the following page, you will be able to select ``GetYourGuide`` as the sales channel type and give it a custom name.
|
||||||
|
|
||||||
|
Preparing your event
|
||||||
|
--------------------
|
||||||
|
In order to now sell your events on GetYourGuide, you will need to configure each event in question.
|
||||||
|
|
||||||
|
1. Enabling the plugin
|
||||||
|
Within your event, extend the ``Settings`` menu and navigate to ``Plugins``. Activate the plugin in the
|
||||||
|
``Integrations`` tab.
|
||||||
|
|
||||||
|
2. Sell the event on the sales channel
|
||||||
|
Pick the sales channel or channels, on which you would like to sell your event by navigating to the event's general
|
||||||
|
settings page using the ``Sell on all sales channels`` or ``Restrict to specific sales channels`` checkboxes.
|
||||||
|
|
||||||
|
3. Configure one or more products to be sold on GetYourGuide
|
||||||
|
Either create a new or edit an existing product, that you would like to sell on GetYourGuide. To do so, you will
|
||||||
|
need to have checked the ``Sell on all sales channels`` or appropriate ``Restrict to specific sales channels``
|
||||||
|
checkbox of the product within it's ``Availability`` tab.
|
||||||
|
In addition, you will also need to set the GetYourGuide equivalent ticket category in the product's accordingly
|
||||||
|
named settings tab. Within your event, there can be only one product per ticket category. Depending on your further
|
||||||
|
configuration, you must at least select one product to be in the ``Adult`` or ``Group`` category.
|
||||||
|
|
||||||
|
4. Configuring the GetYourGuide-plugin
|
||||||
|
Once you have configured one or more products to be eligible to be sold on GetYourGuide, you'll need to configure a
|
||||||
|
few basic settings within the event (``Settings`` --> ``GetYourGuide``). The most important settings can be found
|
||||||
|
the in the ``Configuration`` tab, such as the location of the event on sale.
|
||||||
|
|
||||||
|
Ticket Categories
|
||||||
|
-----------------
|
||||||
|
While pretix only uses the ticket category term loosely to group together multiple products for nicer display,
|
||||||
|
GetYourGuide is relying on the ticket categories to price the tickets.
|
||||||
|
|
||||||
|
First of all, you need to make the decision on how you are planning on selling your tickets on GetYourGuide - in most
|
||||||
|
cases, this will reflect your current sales strategy within your pretix shop.
|
||||||
|
|
||||||
|
- Individual tickets
|
||||||
|
Every single person attending will need to purchase their own ticket. A family of two adults and two
|
||||||
|
children will have to purchase and pay for a total of 4 tickets.
|
||||||
|
In this case, you will need to offer *at least* a ticket of the ``Adult`` type, but may offer any other ticket
|
||||||
|
category type (Child, Youth, Senior, ...) in addition. But you cannot offer a ``Group`` ticket.
|
||||||
|
|
||||||
|
- Group tickets
|
||||||
|
Two groups, consisting of 10 and 20 participants respectively, won't need to purchase a total of 30 tickets, but
|
||||||
|
rather two group tickets. It is up to you to configure the group size limits within the GetYourGuide-settings of your
|
||||||
|
product.
|
||||||
|
Choosing this option, you cannot offer any other ticket categories besides ``Group``.
|
||||||
|
|
||||||
|
Setting up event dates and quotas
|
||||||
|
---------------------------------
|
||||||
|
Of course, in addition to creating products, you will also need to add them to a quota for them to be available for
|
||||||
|
sale. The process for doing this is the very same as for any regular event or event series.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
When selling individual tickets through GetYourGuide, you will not be able to offer differing quantities for
|
||||||
|
individual ticket categories.
|
||||||
|
|
||||||
|
For this reason, we recommend to place all GetYourGuide-eligible products into the same quota. Should you however opt
|
||||||
|
to create multiple quotas which create an imbalance, pretix will report only the available number of tickets for the
|
||||||
|
lowest relevant quota.
|
||||||
|
|
||||||
|
Connecting your event to GetYourGuide
|
||||||
|
-------------------------------------
|
||||||
|
Once you have set up your event and products and performed all necessary configuration, you may want to use the
|
||||||
|
Analyzer-feature of our GetYourGuide-plugin (``Settings`` -> ``GetYourGuide`` -> tab ``Analyzer``).
|
||||||
|
|
||||||
|
The Analyzer should not display any blocking error messages and at least one event date that is ready for publishing on
|
||||||
|
the GetYourGuide platform.
|
||||||
|
|
||||||
|
At this point, you will need to setup your event (called ``product`` in the GetYourGuide universe) on their
|
||||||
|
`Supplier Portal`_ and connect it with your pretix shop. To do so, please follow the
|
||||||
|
`Connecting a new product to your Reservation System`_ on the GetYourGuide Supply Partner Help Center.
|
||||||
|
|
||||||
|
Select ``pretix.eu`` as your reservation system; the required ``product ID`` can be found in the ``Configuration`` tab
|
||||||
|
of the GetYourGuide plugin settings page.
|
||||||
|
|
||||||
|
From this point on, GetYourGuide will automatically import the availabilities and products and offer them for sale.
|
||||||
|
|
||||||
|
.. _Supplier Portal: https://suppliers.getyourguide.com/
|
||||||
|
.. _Connecting a new product to your Reservation System: https://supply.getyourguide.support/hc/en-us/articles/18008029689373-Connecting-a-new-product-to-your-Reservation-system
|
||||||
@@ -25,3 +25,4 @@ If you want to **create** a plugin, please go to the
|
|||||||
webinar
|
webinar
|
||||||
presale-saml
|
presale-saml
|
||||||
kulturpass
|
kulturpass
|
||||||
|
getyourguide
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ item_assignments list of objects Products this l
|
|||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|
||||||
Endpoints
|
Layout endpoints
|
||||||
---------
|
----------------
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/ticketlayouts/
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/ticketlayouts/
|
||||||
|
|
||||||
@@ -268,5 +268,75 @@ Endpoints
|
|||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
|
||||||
|
|
||||||
|
Ticket rendering endpoint
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/ticketpdfrenderer/render_batch/
|
||||||
|
|
||||||
|
With this API call, you can instruct the system to render a set of tickets into one combined PDF file. To specify
|
||||||
|
which tickets to render, you need to submit a list of "parts". For every part, the following fields are supported:
|
||||||
|
|
||||||
|
* ``orderposition`` (``integer``, required): The ID of the order position to render.
|
||||||
|
* ``override_channel`` (``string``, optional): The sales channel ID to be used for layout selection instead of the
|
||||||
|
original channel of the order.
|
||||||
|
* ``override_layout`` (``integer``, optional): The ticket layout ID to be used instead of the auto-selected one.
|
||||||
|
|
||||||
|
If your input parameters validate correctly, a ``202 Accepted`` status code is returned.
|
||||||
|
The body points you to the download URL of the result. Running a ``GET`` request on that result URL will
|
||||||
|
yield one of the following status codes:
|
||||||
|
|
||||||
|
* ``200 OK`` – The export succeeded. The body will be your resulting file. Might be large!
|
||||||
|
* ``409 Conflict`` – Your export is still running. The body will be JSON with the structure ``{"status": "running"}``. ``status`` can be ``waiting`` before the task is actually being processed. Please retry, but wait at least one second before you do.
|
||||||
|
* ``410 Gone`` – Running the export has failed permanently. The body will be JSON with the structure ``{"status": "failed", "message": "Error message"}``
|
||||||
|
* ``404 Not Found`` – The export does not exist / is expired.
|
||||||
|
|
||||||
|
.. warning:: This endpoint is considered **experimental**. It might change at any time without prior notice.
|
||||||
|
|
||||||
|
.. note:: To avoid performance issues, a maximum number of 1000 parts is currently allowed.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/ticketpdfrenderer/render_batch/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"parts": [
|
||||||
|
{
|
||||||
|
"orderposition": 55412
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"orderposition": 55412,
|
||||||
|
"override_channel": "web"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"orderposition": 55412,
|
||||||
|
"override_layout": 56
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"download": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/ticketpdfrenderer/download/29891ede-196f-4942-9e26-d055a36e98b8/3f279f13-c198-4137-b49b-9b360ce9fcce/"
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param event: The ``slug`` field of the event to fetch
|
||||||
|
:statuscode 202: no error
|
||||||
|
:statuscode 400: Invalid input options
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||||
|
|
||||||
|
|
||||||
.. _here: https://github.com/pretix/pretix/blob/master/src/pretix/static/schema/pdf-layout.schema.json
|
.. _here: https://github.com/pretix/pretix/blob/master/src/pretix/static/schema/pdf-layout.schema.json
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
sphinx==7.2.*
|
sphinx==7.4.*
|
||||||
jinja2==3.1.*
|
jinja2==3.1.*
|
||||||
sphinx-rtd-theme
|
sphinx-rtd-theme
|
||||||
sphinxcontrib-httpdomain
|
sphinxcontrib-httpdomain
|
||||||
@@ -6,5 +6,4 @@ sphinxcontrib-images
|
|||||||
sphinxcontrib-jquery
|
sphinxcontrib-jquery
|
||||||
sphinxcontrib-spelling==8.*
|
sphinxcontrib-spelling==8.*
|
||||||
sphinxemoji
|
sphinxemoji
|
||||||
pygments-markdown-lexer
|
|
||||||
pyenchant==3.2.*
|
pyenchant==3.2.*
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
-e ../
|
-e ../
|
||||||
sphinx==7.2.*
|
sphinx==7.4.*
|
||||||
jinja2==3.1.*
|
jinja2==3.1.*
|
||||||
sphinx-rtd-theme
|
sphinx-rtd-theme
|
||||||
sphinxcontrib-httpdomain
|
sphinxcontrib-httpdomain
|
||||||
@@ -7,5 +7,4 @@ sphinxcontrib-images
|
|||||||
sphinxcontrib-jquery
|
sphinxcontrib-jquery
|
||||||
sphinxcontrib-spelling==8.*
|
sphinxcontrib-spelling==8.*
|
||||||
sphinxemoji
|
sphinxemoji
|
||||||
pygments-markdown-lexer
|
|
||||||
pyenchant==3.2.*
|
pyenchant==3.2.*
|
||||||
|
|||||||
110
doc/user/android-version-support.rst
Normal file
110
doc/user/android-version-support.rst
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
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.
|
||||||
|
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.
|
||||||
|
| No support for SumUp.
|
||||||
|
Android 6 | 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.
|
||||||
|
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.
|
||||||
|
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 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.
|
||||||
|
=========================== ==========================================================
|
||||||
@@ -175,7 +175,7 @@ without any special behavior.
|
|||||||
Connecting SSO providers (pretix as the SSO client)
|
Connecting SSO providers (pretix as the SSO client)
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
To connect an external application as a SSO client, go to "Customer accounts" → "SSO providers" → "Create a new SSO provider"
|
To connect an external application as a SSO provider, go to "Customer accounts" → "SSO providers" → "Create a new SSO provider"
|
||||||
in your organizer account.
|
in your organizer account.
|
||||||
|
|
||||||
.. thumbnail:: ../../screens/organizer/customer_ssoprovider_add.png
|
.. thumbnail:: ../../screens/organizer/customer_ssoprovider_add.png
|
||||||
|
|||||||
@@ -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
|
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::
|
``<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">
|
<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></script>
|
<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::
|
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-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
|
* ``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
|
``FIELD`` are ``company``, ``street``, ``zipcode``, ``city``, ``country``, ``internal-reference``, ``vat-id``, and
|
||||||
naming scheme such as ``name-title`` or ``name-given-name`` (see above). ``country`` expects a two-character
|
``custom-field``, as well as fields specified by the naming scheme such as ``name-title`` or ``name-given-name``
|
||||||
country code.
|
(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
|
* 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
|
only works for the order email address as well as the invoice address. Attendee-level fields and questions can
|
||||||
@@ -449,5 +449,37 @@ Further reading:
|
|||||||
|
|
||||||
* `Stripe Payment Method Domain registration`_
|
* `Stripe Payment Method Domain registration`_
|
||||||
|
|
||||||
|
|
||||||
|
Content Security Policy
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
When using a Content Security Policy (CSP) on your website, you may need to make some adjustments. If your pretix
|
||||||
|
shop is running under a custom domain, you need to add the following rules:
|
||||||
|
|
||||||
|
* ``script-src``: ``'unsafe-eval' https://pretix.eu`` (adjust to your domain for self-hosted pretix)
|
||||||
|
* ``style-src``: ``https://pretix.eu`` (adjust to your domain for self-hosted pretix **and** for custom domain on pretix Hosted)
|
||||||
|
* ``connect-src``: ``https://pretix.eu`` (adjust to your domain for self-hosted pretix **and** for custom domain on pretix Hosted)
|
||||||
|
* ``frame-src``: ``https://pretix.eu`` (adjust to your domain for self-hosted pretix **and** for custom domain on pretix Hosted)
|
||||||
|
* ``img-src``: ``https://pretix.eu`` (adjust to your domain for self-hosted pretix **and** for custom domain on pretix Hosted) and for pretix Hosted additionally add ``https://cdn.pretix.space``
|
||||||
|
|
||||||
|
|
||||||
|
External payment providers and Cross-Origin-Opener-Policy
|
||||||
|
---------------------------------------------------------
|
||||||
|
|
||||||
|
If you use a payment provider that opens a new window during checkout (such as PayPal), be aware that setting
|
||||||
|
``Cross-Origin-Opener-Policy: same-origin`` results in an empty popup-window being opened in the foreground. This is
|
||||||
|
due to JavaScript not having access to the opened window. To mitigate this, you either need to always open the widget’s
|
||||||
|
checkout in a new tab (see :ref:`Always open a new tab`) or set ``Cross-Origin-Opener-Policy: same-origin-allow-popups``
|
||||||
|
|
||||||
|
|
||||||
|
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/
|
.. _Let's Encrypt: https://letsencrypt.org/
|
||||||
.. _Stripe Payment Method Domain registration: https://stripe.com/docs/payments/payment-methods/pmd-registration
|
.. _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
|
events/giftcards
|
||||||
faq
|
faq
|
||||||
markdown
|
markdown
|
||||||
|
android-version-support
|
||||||
glossary
|
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
|
since it is way easier to learn than languages like HTML but allows all basic formatting options required
|
||||||
for text in those places.
|
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
|
Formatting rules
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ classifiers = [
|
|||||||
"Programming Language :: Python :: 3.9",
|
"Programming Language :: Python :: 3.9",
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
"Framework :: Django :: 4.1",
|
"Framework :: Django :: 4.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
@@ -30,42 +30,41 @@ dependencies = [
|
|||||||
"babel",
|
"babel",
|
||||||
"BeautifulSoup4==4.12.*",
|
"BeautifulSoup4==4.12.*",
|
||||||
"bleach==5.0.*",
|
"bleach==5.0.*",
|
||||||
"celery==5.3.*",
|
"celery==5.4.*",
|
||||||
"chardet==5.2.*",
|
"chardet==5.2.*",
|
||||||
"cryptography>=3.4.2",
|
"cryptography>=3.4.2",
|
||||||
"css-inline==0.13.*",
|
"css-inline==0.14.*",
|
||||||
"defusedcsv>=1.1.0",
|
"defusedcsv>=1.1.0",
|
||||||
"dj-static",
|
"Django[argon2]==4.2.*,>=4.2.15",
|
||||||
"Django==4.2.*",
|
"django-bootstrap3==24.3",
|
||||||
"django-bootstrap3==23.6.*",
|
"django-compressor==4.5.1",
|
||||||
"django-compressor==4.4",
|
"django-countries==7.6.*",
|
||||||
"django-countries==7.5.*",
|
"django-filter==24.3",
|
||||||
"django-filter==23.5",
|
|
||||||
"django-formset-js-improved==0.5.0.3",
|
"django-formset-js-improved==0.5.0.3",
|
||||||
"django-formtools==2.5.1",
|
"django-formtools==2.5.1",
|
||||||
"django-hierarkey==1.1.*",
|
"django-hierarkey==1.2.*",
|
||||||
"django-hijack==3.4.*",
|
"django-hijack==3.6.*",
|
||||||
"django-i18nfield==1.9.*,>=1.9.4",
|
"django-i18nfield==1.9.*,>=1.9.4",
|
||||||
"django-libsass==0.9",
|
"django-libsass==0.9",
|
||||||
"django-localflavor==4.0",
|
"django-localflavor==4.0",
|
||||||
"django-markup",
|
"django-markup",
|
||||||
"django-oauth-toolkit==2.3.*",
|
"django-oauth-toolkit==2.3.*",
|
||||||
"django-otp==1.3.*",
|
"django-otp==1.5.*",
|
||||||
"django-phonenumber-field==7.3.*",
|
"django-phonenumber-field==7.3.*",
|
||||||
"django-redis==5.4.*",
|
"django-redis==5.4.*",
|
||||||
"django-scopes==2.0.*",
|
"django-scopes==2.0.*",
|
||||||
"django-statici18n==2.4.*",
|
"django-statici18n==2.5.*",
|
||||||
"djangorestframework==3.14.*",
|
"djangorestframework==3.15.*",
|
||||||
"dnspython==2.5.*",
|
"dnspython==2.7.*",
|
||||||
"drf_ujson2==1.7.*",
|
"drf_ujson2==1.7.*",
|
||||||
"geoip2==4.*",
|
"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",
|
"isoweek",
|
||||||
"jsonschema",
|
"jsonschema",
|
||||||
"kombu==5.3.*",
|
"kombu==5.4.*",
|
||||||
"libsass==0.23.*",
|
"libsass==0.23.*",
|
||||||
"lxml",
|
"lxml",
|
||||||
"markdown==3.5.2", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
|
"markdown==3.7", # 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
|
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
|
||||||
"mt-940==4.30.*",
|
"mt-940==4.30.*",
|
||||||
"oauthlib==3.2.*",
|
"oauthlib==3.2.*",
|
||||||
@@ -73,60 +72,60 @@ dependencies = [
|
|||||||
"packaging",
|
"packaging",
|
||||||
"paypalrestsdk==1.13.*",
|
"paypalrestsdk==1.13.*",
|
||||||
"paypal-checkout-serversdk==1.0.*",
|
"paypal-checkout-serversdk==1.0.*",
|
||||||
"PyJWT==2.8.*",
|
"PyJWT==2.9.*",
|
||||||
"phonenumberslite==8.13.*",
|
"phonenumberslite==8.13.*",
|
||||||
"Pillow==10.2.*",
|
"Pillow==10.4.*",
|
||||||
"pretix-plugin-build",
|
"pretix-plugin-build",
|
||||||
"protobuf==4.25.*",
|
"protobuf==5.28.*",
|
||||||
"psycopg2-binary",
|
"psycopg2-binary",
|
||||||
"pycountry",
|
"pycountry",
|
||||||
"pycparser==2.21",
|
"pycparser==2.22",
|
||||||
"pycryptodome==3.20.*",
|
"pycryptodome==3.21.*",
|
||||||
"pypdf==3.9.*",
|
"pypdf==5.0.*",
|
||||||
"python-bidi==0.4.*", # Support for Arabic in reportlab
|
"python-bidi==0.6.*", # Support for Arabic in reportlab
|
||||||
"python-dateutil==2.9.*",
|
"python-dateutil==2.9.*",
|
||||||
"pytz",
|
"pytz",
|
||||||
"pytz-deprecation-shim==0.1.*",
|
"pytz-deprecation-shim==0.1.*",
|
||||||
"pyuca",
|
"pyuca",
|
||||||
"qrcode==7.4.*",
|
"qrcode==8.0",
|
||||||
"redis==5.0.*",
|
"redis==5.1.*",
|
||||||
"reportlab==4.1.*",
|
"reportlab==4.2.*",
|
||||||
"requests==2.31.*",
|
"requests==2.31.*",
|
||||||
"sentry-sdk==1.40.*",
|
"sentry-sdk==2.17.*",
|
||||||
"sepaxml==2.6.*",
|
"sepaxml==2.6.*",
|
||||||
"slimit",
|
"slimit",
|
||||||
"static3==0.7.*",
|
|
||||||
"stripe==7.9.*",
|
"stripe==7.9.*",
|
||||||
"text-unidecode==1.*",
|
"text-unidecode==1.*",
|
||||||
"tlds>=2020041600",
|
"tlds>=2020041600",
|
||||||
"tqdm==4.*",
|
"tqdm==4.*",
|
||||||
|
"ua-parser==0.18.*",
|
||||||
"vat_moss_forked==2020.3.20.0.11.0",
|
"vat_moss_forked==2020.3.20.0.11.0",
|
||||||
"vobject==0.9.*",
|
"vobject==0.9.*",
|
||||||
"webauthn==2.0.*",
|
"webauthn==2.2.*",
|
||||||
"zeep==4.2.*"
|
"zeep==4.2.*"
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
memcached = ["pylibmc"]
|
memcached = ["pylibmc"]
|
||||||
dev = [
|
dev = [
|
||||||
"aiohttp==3.9.*",
|
"aiohttp==3.10.*",
|
||||||
"coverage",
|
"coverage",
|
||||||
"coveralls",
|
"coveralls",
|
||||||
"fakeredis==2.21.*",
|
"fakeredis==2.26.*",
|
||||||
"flake8==7.0.*",
|
"flake8==7.1.*",
|
||||||
"freezegun",
|
"freezegun",
|
||||||
"isort==5.13.*",
|
"isort==5.13.*",
|
||||||
"pep8-naming==0.13.*",
|
"pep8-naming==0.14.*",
|
||||||
"potypo",
|
"potypo",
|
||||||
"pytest-asyncio",
|
"pytest-asyncio",
|
||||||
"pytest-cache",
|
"pytest-cache",
|
||||||
"pytest-cov",
|
"pytest-cov",
|
||||||
"pytest-django==4.*",
|
"pytest-django==4.*",
|
||||||
"pytest-mock==3.12.*",
|
"pytest-mock==3.14.*",
|
||||||
"pytest-rerunfailures==13.*",
|
"pytest-rerunfailures==14.*",
|
||||||
"pytest-sugar",
|
"pytest-sugar",
|
||||||
"pytest-xdist==3.5.*",
|
"pytest-xdist==3.6.*",
|
||||||
"pytest==8.0.*",
|
"pytest==8.3.*",
|
||||||
"responses",
|
"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
|
./manage.py compilemessages
|
||||||
|
|
||||||
localegen:
|
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)
|
./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
|
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
|
# 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/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
__version__ = "2024.3.0.dev0"
|
__version__ = "2024.10.0.dev0"
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ INSTALLED_APPS = [
|
|||||||
'pretix.plugins.badges',
|
'pretix.plugins.badges',
|
||||||
'pretix.plugins.manualpayment',
|
'pretix.plugins.manualpayment',
|
||||||
'pretix.plugins.returnurl',
|
'pretix.plugins.returnurl',
|
||||||
|
'pretix.plugins.autocheckin',
|
||||||
'pretix.plugins.webcheckin',
|
'pretix.plugins.webcheckin',
|
||||||
'django_countries',
|
'django_countries',
|
||||||
'oauth2_provider',
|
'oauth2_provider',
|
||||||
@@ -79,6 +80,8 @@ ALL_LANGUAGES = [
|
|||||||
('de', _('German')),
|
('de', _('German')),
|
||||||
('de-informal', _('German (informal)')),
|
('de-informal', _('German (informal)')),
|
||||||
('ar', _('Arabic')),
|
('ar', _('Arabic')),
|
||||||
|
('eu', _('Basque')),
|
||||||
|
('ca', _('Catalan')),
|
||||||
('zh-hans', _('Chinese (simplified)')),
|
('zh-hans', _('Chinese (simplified)')),
|
||||||
('zh-hant', _('Chinese (traditional)')),
|
('zh-hant', _('Chinese (traditional)')),
|
||||||
('cs', _('Czech')),
|
('cs', _('Czech')),
|
||||||
@@ -98,6 +101,8 @@ ALL_LANGUAGES = [
|
|||||||
('pt-br', _('Portuguese (Brazil)')),
|
('pt-br', _('Portuguese (Brazil)')),
|
||||||
('ro', _('Romanian')),
|
('ro', _('Romanian')),
|
||||||
('ru', _('Russian')),
|
('ru', _('Russian')),
|
||||||
|
('sk', _('Slovak')),
|
||||||
|
('sv', _('Swedish')),
|
||||||
('es', _('Spanish')),
|
('es', _('Spanish')),
|
||||||
('tr', _('Turkish')),
|
('tr', _('Turkish')),
|
||||||
('uk', _('Ukrainian')),
|
('uk', _('Ukrainian')),
|
||||||
@@ -111,6 +116,7 @@ LANGUAGES_RTL = {
|
|||||||
LANGUAGES_INCUBATING = {
|
LANGUAGES_INCUBATING = {
|
||||||
'fi', 'pt-br', 'gl',
|
'fi', 'pt-br', 'gl',
|
||||||
}
|
}
|
||||||
|
LANGUAGES = ALL_LANGUAGES
|
||||||
LOCALE_PATHS = [
|
LOCALE_PATHS = [
|
||||||
os.path.join(os.path.dirname(__file__), 'locale'),
|
os.path.join(os.path.dirname(__file__), 'locale'),
|
||||||
]
|
]
|
||||||
@@ -234,7 +240,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 = {
|
CURRENCY_PLACES = {
|
||||||
# default is 2
|
# default is 2
|
||||||
'BIF': 0,
|
'BIF': 0,
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ from pretix.base.models import Device, Event, User
|
|||||||
from pretix.base.models.auth import SuperuserPermissionSet
|
from pretix.base.models.auth import SuperuserPermissionSet
|
||||||
from pretix.base.models.organizer import TeamAPIToken
|
from pretix.base.models.organizer import TeamAPIToken
|
||||||
from pretix.helpers.security import (
|
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
|
return False
|
||||||
except SessionReauthRequired:
|
except SessionReauthRequired:
|
||||||
return False
|
return False
|
||||||
|
except Session2FASetupRequired:
|
||||||
|
return False
|
||||||
|
except SessionPasswordChangeRequired:
|
||||||
|
return False
|
||||||
|
|
||||||
perm_holder = (request.auth if isinstance(request.auth, (Device, TeamAPIToken))
|
perm_holder = (request.auth if isinstance(request.auth, (Device, TeamAPIToken))
|
||||||
else request.user)
|
else request.user)
|
||||||
@@ -144,6 +149,10 @@ class ProfilePermission(BasePermission):
|
|||||||
return False
|
return False
|
||||||
except SessionReauthRequired:
|
except SessionReauthRequired:
|
||||||
return False
|
return False
|
||||||
|
except Session2FASetupRequired:
|
||||||
|
return False
|
||||||
|
except SessionPasswordChangeRequired:
|
||||||
|
return False
|
||||||
|
|
||||||
if isinstance(request.auth, OAuthAccessToken):
|
if isinstance(request.auth, OAuthAccessToken):
|
||||||
if not (request.auth.allow_scopes(['read']) or request.auth.allow_scopes(['profile'])) and request.method in SAFE_METHODS:
|
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
|
return False
|
||||||
except SessionReauthRequired:
|
except SessionReauthRequired:
|
||||||
return False
|
return False
|
||||||
|
except Session2FASetupRequired:
|
||||||
|
return False
|
||||||
|
except SessionPasswordChangeRequired:
|
||||||
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|||||||
82
src/pretix/api/filters.py
Normal file
82
src/pretix/api/filters.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
#
|
||||||
|
# This file is part of pretix (Community Edition).
|
||||||
|
#
|
||||||
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
#
|
||||||
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||||
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||||
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||||
|
# this file, see <https://pretix.eu/about/en/license>.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||||
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from django import forms
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.db.models.constants import LOOKUP_SEP
|
||||||
|
from django.forms import MultipleChoiceField
|
||||||
|
from django_filters import Filter
|
||||||
|
from django_filters.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
class MultipleCharField(forms.CharField):
|
||||||
|
widget = forms.MultipleHiddenInput
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
if not value:
|
||||||
|
return []
|
||||||
|
elif not isinstance(value, (list, tuple)):
|
||||||
|
raise ValidationError(
|
||||||
|
MultipleChoiceField.default_error_messages["invalid_list"], code="invalid_list"
|
||||||
|
)
|
||||||
|
return [str(val) for val in value]
|
||||||
|
|
||||||
|
|
||||||
|
class MultipleCharFilter(Filter):
|
||||||
|
"""
|
||||||
|
This filter performs OR(by default) or AND(using conjoined=True) query
|
||||||
|
on the selected inputs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
field_class = MultipleCharField
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.conjoined = kwargs.pop("conjoined", False)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def filter(self, qs, value):
|
||||||
|
if not value:
|
||||||
|
# Even though not a noop, no point filtering if empty.
|
||||||
|
return qs
|
||||||
|
|
||||||
|
if not self.conjoined:
|
||||||
|
q = Q()
|
||||||
|
for v in set(value):
|
||||||
|
predicate = self.get_filter_predicate(v)
|
||||||
|
if self.conjoined:
|
||||||
|
qs = self.get_method(qs)(**predicate)
|
||||||
|
else:
|
||||||
|
q |= Q(**predicate)
|
||||||
|
|
||||||
|
if not self.conjoined:
|
||||||
|
qs = self.get_method(qs)(q)
|
||||||
|
|
||||||
|
return qs.distinct() if self.distinct else qs
|
||||||
|
|
||||||
|
def get_filter_predicate(self, v):
|
||||||
|
name = self.field_name
|
||||||
|
if name and self.lookup_expr != settings.DEFAULT_LOOKUP_EXPR:
|
||||||
|
name = LOOKUP_SEP.join([name, self.lookup_expr])
|
||||||
|
try:
|
||||||
|
return {name: getattr(v, self.field.to_field_name)}
|
||||||
|
except (AttributeError, TypeError):
|
||||||
|
return {name: v}
|
||||||
@@ -21,7 +21,9 @@
|
|||||||
#
|
#
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from django.db.models import prefetch_related_objects
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
class AsymmetricField(serializers.Field):
|
class AsymmetricField(serializers.Field):
|
||||||
@@ -61,3 +63,67 @@ class CompatibleJSONField(serializers.JSONField):
|
|||||||
if value:
|
if value:
|
||||||
return json.loads(value)
|
return json.loads(value)
|
||||||
return 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:
|
||||||
|
prefetch_related_objects([self.organizer], "sales_channels")
|
||||||
|
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({
|
||||||
|
"limit_sales_channels": [
|
||||||
|
"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({
|
||||||
|
"limit_sales_channels": [
|
||||||
|
"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"]
|
||||||
|
|
||||||
|
if data.get("all_sales_channels"):
|
||||||
|
data["limit_sales_channels"] = []
|
||||||
|
|
||||||
|
return super().to_internal_value(data)
|
||||||
|
|
||||||
|
def to_representation(self, value):
|
||||||
|
value = super().to_representation(value)
|
||||||
|
if value.get("all_sales_channels"):
|
||||||
|
prefetch_related_objects([self.organizer], "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 (
|
from pretix.api.serializers.order import (
|
||||||
AnswerCreateSerializer, AnswerSerializer, InlineSeatSerializer,
|
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
|
from pretix.base.models.orders import CartPosition
|
||||||
|
|
||||||
|
|
||||||
@@ -212,7 +212,11 @@ class CartPositionCreateSerializer(BaseCartPositionCreateSerializer):
|
|||||||
addons = BaseCartPositionCreateSerializer(many=True, required=False)
|
addons = BaseCartPositionCreateSerializer(many=True, required=False)
|
||||||
bundled = BaseCartPositionCreateSerializer(many=True, required=False)
|
bundled = BaseCartPositionCreateSerializer(many=True, required=False)
|
||||||
seat = serializers.CharField(required=False, allow_null=True)
|
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)
|
voucher = serializers.CharField(required=False, allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -221,6 +225,10 @@ class CartPositionCreateSerializer(BaseCartPositionCreateSerializer):
|
|||||||
'cart_id', 'expires', 'addons', 'bundled', 'seat', 'sales_channel', 'voucher'
|
'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):
|
def validate_cart_id(self, cid):
|
||||||
if cid and not cid.endswith('@api'):
|
if cid and not cid.endswith('@api'):
|
||||||
raise ValidationError('Cart ID should end in @api or be empty.')
|
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.event import SubEventSerializer
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
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.media import MEDIA_TYPES
|
||||||
from pretix.base.models import Checkin, CheckinList
|
from pretix.base.models import Checkin, CheckinList, SalesChannel
|
||||||
|
|
||||||
|
|
||||||
class CheckinListSerializer(I18nAwareModelSerializer):
|
class CheckinListSerializer(I18nAwareModelSerializer):
|
||||||
checkin_count = serializers.IntegerField(read_only=True)
|
checkin_count = serializers.IntegerField(read_only=True)
|
||||||
position_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:
|
class Meta:
|
||||||
model = CheckinList
|
model = CheckinList
|
||||||
@@ -43,6 +49,8 @@ class CheckinListSerializer(I18nAwareModelSerializer):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*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'):
|
if 'subevent' in self.context['request'].query_params.getlist('expand'):
|
||||||
self.fields['subevent'] = SubEventSerializer(read_only=True)
|
self.fields['subevent'] = SubEventSerializer(read_only=True)
|
||||||
|
|
||||||
@@ -72,10 +80,6 @@ class CheckinListSerializer(I18nAwareModelSerializer):
|
|||||||
if full_data.get('subevent'):
|
if full_data.get('subevent'):
|
||||||
raise ValidationError(_('The subevent does not belong to this event.'))
|
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'))
|
CheckinList.validate_rules(data.get('rules'))
|
||||||
|
|
||||||
return data
|
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
|
# 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/>.
|
# <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.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:
|
class Meta:
|
||||||
model = Discount
|
model = Discount
|
||||||
fields = ('id', 'active', 'internal_name', 'position', 'sales_channels', 'available_from',
|
fields = ('id', 'active', 'internal_name', 'position', 'all_sales_channels', 'limit_sales_channels',
|
||||||
'available_until', 'subevent_mode', 'condition_all_products', 'condition_limit_products',
|
'available_from', 'available_until', 'subevent_mode', 'condition_all_products',
|
||||||
'condition_apply_to_addons', 'condition_min_count', 'condition_min_value',
|
'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_discount_matching_percent', 'benefit_only_apply_to_cheapest_n_matches',
|
||||||
'benefit_same_products', 'benefit_limit_products', 'benefit_apply_to_addons',
|
'benefit_same_products', 'benefit_limit_products', 'benefit_apply_to_addons',
|
||||||
'benefit_ignore_voucher_discounted', 'condition_ignore_voucher_discounted')
|
'benefit_ignore_voucher_discounted', 'condition_ignore_voucher_discounted')
|
||||||
@@ -39,6 +48,7 @@ class DiscountSerializer(I18nAwareModelSerializer):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['condition_limit_products'].queryset = self.context['event'].items.all()
|
self.fields['condition_limit_products'].queryset = self.context['event'].items.all()
|
||||||
self.fields['benefit_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):
|
def validate(self, data):
|
||||||
data = super().validate(data)
|
data = super().validate(data)
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import PermissionDenied, ValidationError
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
@@ -46,10 +46,15 @@ from rest_framework import serializers
|
|||||||
from rest_framework.fields import ChoiceField, Field
|
from rest_framework.fields import ChoiceField, Field
|
||||||
from rest_framework.relations import SlugRelatedField
|
from rest_framework.relations import SlugRelatedField
|
||||||
|
|
||||||
from pretix.api.serializers import CompatibleJSONField
|
from pretix.api.serializers import (
|
||||||
|
CompatibleJSONField, SalesChannelMigrationMixin,
|
||||||
|
)
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.api.serializers.settings import SettingsSerializer
|
from pretix.api.serializers.settings import SettingsSerializer
|
||||||
from pretix.base.models import Device, Event, TaxRule, TeamAPIToken
|
from pretix.base.models import (
|
||||||
|
CartPosition, Device, Event, OrderPosition, SalesChannel, Seat, TaxRule,
|
||||||
|
TeamAPIToken, Voucher,
|
||||||
|
)
|
||||||
from pretix.base.models.event import SubEvent
|
from pretix.base.models.event import SubEvent
|
||||||
from pretix.base.models.items import (
|
from pretix.base.models.items import (
|
||||||
ItemMetaProperty, SubEventItem, SubEventItemVariation,
|
ItemMetaProperty, SubEventItem, SubEventItemVariation,
|
||||||
@@ -161,7 +166,7 @@ class ValidKeysField(Field):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class EventSerializer(I18nAwareModelSerializer):
|
class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||||
meta_data = MetaDataField(required=False, source='*')
|
meta_data = MetaDataField(required=False, source='*')
|
||||||
item_meta_properties = MetaPropertyField(required=False, source='*')
|
item_meta_properties = MetaPropertyField(required=False, source='*')
|
||||||
plugins = PluginsField(required=False, source='*')
|
plugins = PluginsField(required=False, source='*')
|
||||||
@@ -170,6 +175,13 @@ class EventSerializer(I18nAwareModelSerializer):
|
|||||||
valid_keys = ValidKeysField(source='*', read_only=True)
|
valid_keys = ValidKeysField(source='*', read_only=True)
|
||||||
best_availability_state = serializers.IntegerField(allow_null=True, read_only=True)
|
best_availability_state = serializers.IntegerField(allow_null=True, read_only=True)
|
||||||
public_url = serializers.SerializerMethodField('get_event_url', 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):
|
def get_event_url(self, event):
|
||||||
return build_absolute_uri(event, 'presale:event.index')
|
return build_absolute_uri(event, 'presale:event.index')
|
||||||
@@ -180,7 +192,7 @@ class EventSerializer(I18nAwareModelSerializer):
|
|||||||
'date_to', 'date_admission', 'is_public', 'presale_start',
|
'date_to', 'date_admission', 'is_public', 'presale_start',
|
||||||
'presale_end', 'location', 'geo_lat', 'geo_lon', 'has_subevents', 'meta_data', 'seating_plan',
|
'presale_end', 'location', 'geo_lat', 'geo_lon', 'has_subevents', 'meta_data', 'seating_plan',
|
||||||
'plugins', 'seat_category_mapping', 'timezone', 'item_meta_properties', 'valid_keys',
|
'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):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -188,6 +200,7 @@ class EventSerializer(I18nAwareModelSerializer):
|
|||||||
self.fields.pop('valid_keys')
|
self.fields.pop('valid_keys')
|
||||||
if not self.context.get('request') or 'with_availability_for' not in self.context['request'].GET:
|
if not self.context.get('request') or 'with_availability_for' not in self.context['request'].GET:
|
||||||
self.fields.pop('best_availability_state')
|
self.fields.pop('best_availability_state')
|
||||||
|
self.fields['limit_sales_channels'].child_relation.queryset = self.context['organizer'].sales_channels.all()
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
data = super().validate(data)
|
data = super().validate(data)
|
||||||
@@ -269,13 +282,17 @@ class EventSerializer(I18nAwareModelSerializer):
|
|||||||
from pretix.base.plugins import get_all_plugins
|
from pretix.base.plugins import get_all_plugins
|
||||||
|
|
||||||
plugins_available = {
|
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)
|
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'):
|
for plugin in value.get('plugins'):
|
||||||
if plugin not in plugins_available:
|
if plugin not in plugins_available:
|
||||||
raise ValidationError(_('Unknown plugin: \'{name}\'.').format(name=plugin))
|
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
|
return value
|
||||||
|
|
||||||
@@ -472,7 +489,8 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
|||||||
fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission',
|
fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission',
|
||||||
'presale_start', 'presale_end', 'location', 'geo_lat', 'geo_lon', 'event', 'is_public',
|
'presale_start', 'presale_end', 'location', 'geo_lat', 'geo_lon', 'event', 'is_public',
|
||||||
'frontpage_text', 'seating_plan', 'item_price_overrides', 'variation_price_overrides',
|
'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):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -683,10 +701,12 @@ class EventSettingsSerializer(SettingsSerializer):
|
|||||||
'locales',
|
'locales',
|
||||||
'locale',
|
'locale',
|
||||||
'region',
|
'region',
|
||||||
'last_order_modification_date',
|
'allow_modifications',
|
||||||
'allow_modifications_after_checkin',
|
'allow_modifications_after_checkin',
|
||||||
|
'last_order_modification_date',
|
||||||
'show_quota_left',
|
'show_quota_left',
|
||||||
'waiting_list_enabled',
|
'waiting_list_enabled',
|
||||||
|
'waiting_list_auto_disable',
|
||||||
'waiting_list_hours',
|
'waiting_list_hours',
|
||||||
'waiting_list_auto',
|
'waiting_list_auto',
|
||||||
'waiting_list_names_asked',
|
'waiting_list_names_asked',
|
||||||
@@ -733,6 +753,7 @@ class EventSettingsSerializer(SettingsSerializer):
|
|||||||
'payment_term_accept_late',
|
'payment_term_accept_late',
|
||||||
'payment_explanation',
|
'payment_explanation',
|
||||||
'payment_pending_hidden',
|
'payment_pending_hidden',
|
||||||
|
'payment_giftcard__enabled',
|
||||||
'mail_days_order_expire_warning',
|
'mail_days_order_expire_warning',
|
||||||
'ticket_download',
|
'ticket_download',
|
||||||
'ticket_download_date',
|
'ticket_download_date',
|
||||||
@@ -751,6 +772,7 @@ class EventSettingsSerializer(SettingsSerializer):
|
|||||||
'invoice_address_company_required',
|
'invoice_address_company_required',
|
||||||
'invoice_address_beneficiary',
|
'invoice_address_beneficiary',
|
||||||
'invoice_address_custom_field',
|
'invoice_address_custom_field',
|
||||||
|
'invoice_address_custom_field_helptext',
|
||||||
'invoice_name_required',
|
'invoice_name_required',
|
||||||
'invoice_address_not_asked_free',
|
'invoice_address_not_asked_free',
|
||||||
'invoice_show_payments',
|
'invoice_show_payments',
|
||||||
@@ -824,6 +846,7 @@ class EventSettingsSerializer(SettingsSerializer):
|
|||||||
'reusable_media_type_nfc_mf0aes_autocreate_giftcard',
|
'reusable_media_type_nfc_mf0aes_autocreate_giftcard',
|
||||||
'reusable_media_type_nfc_mf0aes_autocreate_giftcard_currency',
|
'reusable_media_type_nfc_mf0aes_autocreate_giftcard_currency',
|
||||||
'reusable_media_type_nfc_mf0aes_random_uid',
|
'reusable_media_type_nfc_mf0aes_random_uid',
|
||||||
|
'seating_allow_blocked_seats_for_channel',
|
||||||
]
|
]
|
||||||
readonly_fields = [
|
readonly_fields = [
|
||||||
# These are read-only since they are currently only settable on organizers, not events
|
# These are read-only since they are currently only settable on organizers, not events
|
||||||
@@ -874,6 +897,7 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
|
|||||||
'locale',
|
'locale',
|
||||||
'last_order_modification_date',
|
'last_order_modification_date',
|
||||||
'show_quota_left',
|
'show_quota_left',
|
||||||
|
'show_dates_on_frontpage',
|
||||||
'max_items_per_order',
|
'max_items_per_order',
|
||||||
'attendee_names_asked',
|
'attendee_names_asked',
|
||||||
'attendee_names_required',
|
'attendee_names_required',
|
||||||
@@ -893,6 +917,7 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
|
|||||||
'invoice_address_company_required',
|
'invoice_address_company_required',
|
||||||
'invoice_address_beneficiary',
|
'invoice_address_beneficiary',
|
||||||
'invoice_address_custom_field',
|
'invoice_address_custom_field',
|
||||||
|
'invoice_address_custom_field_helptext',
|
||||||
'invoice_name_required',
|
'invoice_name_required',
|
||||||
'invoice_address_not_asked_free',
|
'invoice_address_not_asked_free',
|
||||||
'invoice_address_from_name',
|
'invoice_address_from_name',
|
||||||
@@ -949,3 +974,77 @@ class ItemMetaPropertiesSerializer(I18nAwareModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = ItemMetaProperty
|
model = ItemMetaProperty
|
||||||
fields = ('id', 'name', 'default', 'required', 'allowed_values')
|
fields = ('id', 'name', 'default', 'required', 'allowed_values')
|
||||||
|
|
||||||
|
|
||||||
|
def prefetch_by_id(items, qs, id_attr, target_attr):
|
||||||
|
"""
|
||||||
|
Prefetches a related object on each item in the given list of items by searching by id or another
|
||||||
|
unique field. The id value is read from the attribute on item specified in `id_attr`, searched on queryset `qs` by
|
||||||
|
the primary key, and the resulting prefetched model object is stored into `target_attr` on the item.
|
||||||
|
"""
|
||||||
|
ids = [getattr(item, id_attr) for item in items if getattr(item, id_attr)]
|
||||||
|
if ids:
|
||||||
|
result = qs.in_bulk(id_list=ids)
|
||||||
|
for item in items:
|
||||||
|
setattr(item, target_attr, result.get(getattr(item, id_attr)))
|
||||||
|
|
||||||
|
|
||||||
|
class SeatSerializer(I18nAwareModelSerializer):
|
||||||
|
orderposition = serializers.IntegerField(source='orderposition_id')
|
||||||
|
cartposition = serializers.IntegerField(source='cartposition_id')
|
||||||
|
voucher = serializers.IntegerField(source='voucher_id')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Seat
|
||||||
|
read_only_fields = (
|
||||||
|
'id', 'subevent', 'zone_name', 'row_name', 'row_label',
|
||||||
|
'seat_number', 'seat_label', 'seat_guid', 'product',
|
||||||
|
'orderposition', 'cartposition', 'voucher',
|
||||||
|
)
|
||||||
|
fields = (
|
||||||
|
'id', 'subevent', 'zone_name', 'row_name', 'row_label',
|
||||||
|
'seat_number', 'seat_label', 'seat_guid', 'product', 'blocked',
|
||||||
|
'orderposition', 'cartposition', 'voucher',
|
||||||
|
)
|
||||||
|
|
||||||
|
def prefetch_expanded_data(self, items, request, expand_fields):
|
||||||
|
if 'orderposition' in expand_fields:
|
||||||
|
if 'can_view_orders' not in request.eventpermset:
|
||||||
|
raise PermissionDenied('can_view_orders permission required for expand=orderposition')
|
||||||
|
prefetch_by_id(items, OrderPosition.objects.prefetch_related('order'), 'orderposition_id', 'orderposition')
|
||||||
|
if 'cartposition' in expand_fields:
|
||||||
|
if 'can_view_orders' not in request.eventpermset:
|
||||||
|
raise PermissionDenied('can_view_orders permission required for expand=cartposition')
|
||||||
|
prefetch_by_id(items, CartPosition.objects, 'cartposition_id', 'cartposition')
|
||||||
|
if 'voucher' in expand_fields:
|
||||||
|
if 'can_view_vouchers' not in request.eventpermset:
|
||||||
|
raise PermissionDenied('can_view_vouchers permission required for expand=voucher')
|
||||||
|
prefetch_by_id(items, Voucher.objects, 'voucher_id', 'voucher')
|
||||||
|
|
||||||
|
def __init__(self, instance, *args, **kwargs):
|
||||||
|
if not kwargs.get('data'):
|
||||||
|
self.prefetch_expanded_data(instance if hasattr(instance, '__iter__') else [instance],
|
||||||
|
kwargs['context']['request'],
|
||||||
|
kwargs['context']['expand_fields'])
|
||||||
|
|
||||||
|
super().__init__(instance, *args, **kwargs)
|
||||||
|
|
||||||
|
if 'orderposition' in self.context['expand_fields']:
|
||||||
|
from pretix.api.serializers.media import (
|
||||||
|
NestedOrderPositionSerializer,
|
||||||
|
)
|
||||||
|
self.fields['orderposition'] = NestedOrderPositionSerializer(read_only=True, context=self.context['order_context'])
|
||||||
|
try:
|
||||||
|
del self.fields['orderposition'].fields['seat']
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if 'cartposition' in self.context['expand_fields']:
|
||||||
|
from pretix.api.serializers.cart import CartPositionSerializer
|
||||||
|
self.fields['cartposition'] = CartPositionSerializer(read_only=True)
|
||||||
|
del self.fields['cartposition'].fields['seat']
|
||||||
|
|
||||||
|
if 'voucher' in self.context['expand_fields']:
|
||||||
|
from pretix.api.serializers.voucher import VoucherSerializer
|
||||||
|
self.fields['voucher'] = VoucherSerializer(read_only=True)
|
||||||
|
del self.fields['voucher'].fields['seat']
|
||||||
|
|||||||
@@ -42,19 +42,27 @@ from django.utils.functional import cached_property, lazy
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from pretix.api.serializers import SalesChannelMigrationMixin
|
||||||
from pretix.api.serializers.event import MetaDataField
|
from pretix.api.serializers.event import MetaDataField
|
||||||
from pretix.api.serializers.fields import UploadedFileField
|
from pretix.api.serializers.fields import UploadedFileField
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemVariation,
|
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,
|
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=13,
|
||||||
coerce_to_string=True)
|
coerce_to_string=True)
|
||||||
meta_data = MetaDataField(required=False, source='*')
|
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:
|
class Meta:
|
||||||
model = ItemVariation
|
model = ItemVariation
|
||||||
@@ -63,11 +71,14 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
|
|||||||
'require_membership', 'require_membership_types', 'require_membership_hidden',
|
'require_membership', 'require_membership_types', 'require_membership_hidden',
|
||||||
'checkin_attention', 'checkin_text',
|
'checkin_attention', 'checkin_text',
|
||||||
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
||||||
'sales_channels', 'hide_without_voucher', 'meta_data')
|
'all_sales_channels', 'limit_sales_channels', 'hide_without_voucher', 'meta_data')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['require_membership_types'].queryset = lazy(lambda: self.context['event'].organizer.membership_types.all(), QuerySet)
|
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):
|
def validate_meta_data(self, value):
|
||||||
for key in value['meta_data'].keys():
|
for key in value['meta_data'].keys():
|
||||||
@@ -76,10 +87,17 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
class ItemVariationSerializer(I18nAwareModelSerializer):
|
class ItemVariationSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||||
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=13,
|
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=13,
|
||||||
coerce_to_string=True)
|
coerce_to_string=True)
|
||||||
meta_data = MetaDataField(required=False, source='*')
|
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:
|
class Meta:
|
||||||
model = ItemVariation
|
model = ItemVariation
|
||||||
@@ -88,21 +106,26 @@ class ItemVariationSerializer(I18nAwareModelSerializer):
|
|||||||
'require_membership', 'require_membership_types', 'require_membership_hidden',
|
'require_membership', 'require_membership_types', 'require_membership_hidden',
|
||||||
'checkin_attention', 'checkin_text',
|
'checkin_attention', 'checkin_text',
|
||||||
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
||||||
'sales_channels', 'hide_without_voucher', 'meta_data')
|
'all_sales_channels', 'limit_sales_channels', 'hide_without_voucher', 'meta_data')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['require_membership_types'].queryset = self.context['event'].organizer.membership_types.all()
|
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
|
@transaction.atomic
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
meta_data = validated_data.pop('meta_data', None)
|
meta_data = validated_data.pop('meta_data', None)
|
||||||
require_membership_types = validated_data.pop('require_membership_types', [])
|
require_membership_types = validated_data.pop('require_membership_types', [])
|
||||||
|
limit_sales_channels = validated_data.pop('limit_sales_channels', [])
|
||||||
variation = ItemVariation.objects.create(**validated_data)
|
variation = ItemVariation.objects.create(**validated_data)
|
||||||
|
|
||||||
if require_membership_types:
|
if require_membership_types:
|
||||||
variation.require_membership_types.add(*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
|
# Meta data
|
||||||
if meta_data is not None:
|
if meta_data is not None:
|
||||||
for key, value in meta_data.items():
|
for key, value in meta_data.items():
|
||||||
@@ -223,7 +246,7 @@ class ItemTaxRateField(serializers.Field):
|
|||||||
return str(Decimal('0.00'))
|
return str(Decimal('0.00'))
|
||||||
|
|
||||||
|
|
||||||
class ItemSerializer(I18nAwareModelSerializer):
|
class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||||
addons = InlineItemAddOnSerializer(many=True, required=False)
|
addons = InlineItemAddOnSerializer(many=True, required=False)
|
||||||
bundles = InlineItemBundleSerializer(many=True, required=False)
|
bundles = InlineItemBundleSerializer(many=True, required=False)
|
||||||
variations = InlineItemVariationSerializer(many=True, required=False)
|
variations = InlineItemVariationSerializer(many=True, required=False)
|
||||||
@@ -232,11 +255,18 @@ class ItemSerializer(I18nAwareModelSerializer):
|
|||||||
picture = UploadedFileField(required=False, allow_null=True, allowed_types=(
|
picture = UploadedFileField(required=False, allow_null=True, allowed_types=(
|
||||||
'image/png', 'image/jpeg', 'image/gif'
|
'image/png', 'image/jpeg', 'image/gif'
|
||||||
), max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE)
|
), 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:
|
class Meta:
|
||||||
model = Item
|
model = Item
|
||||||
fields = ('id', 'category', 'name', 'internal_name', 'active', 'sales_channels', 'description',
|
fields = ('id', 'category', 'name', 'internal_name', 'active', 'all_sales_channels', 'limit_sales_channels',
|
||||||
'default_price', 'free_price', 'free_price_suggestion', 'tax_rate', 'tax_rule', 'admission',
|
'description', 'default_price', 'free_price', 'free_price_suggestion', 'tax_rate', 'tax_rule', 'admission',
|
||||||
'personalized', 'position', 'picture',
|
'personalized', 'position', 'picture',
|
||||||
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
||||||
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
|
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
|
||||||
@@ -259,6 +289,8 @@ class ItemSerializer(I18nAwareModelSerializer):
|
|||||||
if not self.read_only:
|
if not self.read_only:
|
||||||
self.fields['require_membership_types'].queryset = self.context['event'].organizer.membership_types.all()
|
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['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):
|
def validate(self, data):
|
||||||
data = super().validate(data)
|
data = super().validate(data)
|
||||||
@@ -335,7 +367,10 @@ class ItemSerializer(I18nAwareModelSerializer):
|
|||||||
meta_data = validated_data.pop('meta_data', None)
|
meta_data = validated_data.pop('meta_data', None)
|
||||||
picture = validated_data.pop('picture', None)
|
picture = validated_data.pop('picture', None)
|
||||||
require_membership_types = validated_data.pop('require_membership_types', [])
|
require_membership_types = validated_data.pop('require_membership_types', [])
|
||||||
|
limit_sales_channels = validated_data.pop('limit_sales_channels', [])
|
||||||
item = Item.objects.create(**validated_data)
|
item = Item.objects.create(**validated_data)
|
||||||
|
if limit_sales_channels and not validated_data.get('all_sales_channels'):
|
||||||
|
item.limit_sales_channels.add(*limit_sales_channels)
|
||||||
if picture:
|
if picture:
|
||||||
item.picture.save(os.path.basename(picture.name), picture)
|
item.picture.save(os.path.basename(picture.name), picture)
|
||||||
if require_membership_types:
|
if require_membership_types:
|
||||||
@@ -343,10 +378,13 @@ class ItemSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
for variation_data in variations_data:
|
for variation_data in variations_data:
|
||||||
require_membership_types = variation_data.pop('require_membership_types', [])
|
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', {})
|
var_meta_data = variation_data.pop('meta_data', {})
|
||||||
v = ItemVariation.objects.create(item=item, **variation_data)
|
v = ItemVariation.objects.create(item=item, **variation_data)
|
||||||
if require_membership_types:
|
if require_membership_types:
|
||||||
v.require_membership_types.add(*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:
|
if var_meta_data is not None:
|
||||||
for key, value in var_meta_data.items():
|
for key, value in var_meta_data.items():
|
||||||
@@ -403,7 +441,22 @@ class ItemCategorySerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ItemCategory
|
model = ItemCategory
|
||||||
fields = ('id', 'name', 'internal_name', 'description', 'position', 'is_addon')
|
fields = (
|
||||||
|
'id', 'name', 'internal_name', 'description', 'position',
|
||||||
|
'is_addon', 'cross_selling_mode',
|
||||||
|
'cross_selling_condition', 'cross_selling_match_products'
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
data = super().validate(data)
|
||||||
|
|
||||||
|
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
||||||
|
full_data.update(data)
|
||||||
|
|
||||||
|
if full_data.get('is_addon') and full_data.get('cross_selling_mode'):
|
||||||
|
raise ValidationError('is_addon and cross_selling_mode are mutually exclusive')
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
class QuestionOptionSerializer(I18nAwareModelSerializer):
|
class QuestionOptionSerializer(I18nAwareModelSerializer):
|
||||||
|
|||||||
@@ -46,13 +46,12 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
|||||||
from pretix.api.serializers.item import (
|
from pretix.api.serializers.item import (
|
||||||
InlineItemVariationSerializer, ItemSerializer, QuestionSerializer,
|
InlineItemVariationSerializer, ItemSerializer, QuestionSerializer,
|
||||||
)
|
)
|
||||||
from pretix.base.channels import get_all_sales_channels
|
|
||||||
from pretix.base.decimal import round_decimal
|
from pretix.base.decimal import round_decimal
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CachedFile, Checkin, Customer, Invoice, InvoiceAddress, InvoiceLine, Item,
|
CachedFile, Checkin, Customer, Invoice, InvoiceAddress, InvoiceLine, Item,
|
||||||
ItemVariation, Order, OrderPosition, Question, QuestionAnswer,
|
ItemVariation, Order, OrderPosition, Question, QuestionAnswer,
|
||||||
ReusableMedium, Seat, SubEvent, TaxRule, Voucher,
|
ReusableMedium, SalesChannel, Seat, SubEvent, TaxRule, Voucher,
|
||||||
)
|
)
|
||||||
from pretix.base.models.orders import (
|
from pretix.base.models.orders import (
|
||||||
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
|
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
|
||||||
@@ -166,7 +165,7 @@ class InlineSeatSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Seat
|
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):
|
class AnswerSerializer(I18nAwareModelSerializer):
|
||||||
@@ -274,9 +273,15 @@ class AnswerSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class CheckinSerializer(I18nAwareModelSerializer):
|
class CheckinSerializer(I18nAwareModelSerializer):
|
||||||
|
device_id = serializers.SlugRelatedField(
|
||||||
|
source='device',
|
||||||
|
slug_field='device_id',
|
||||||
|
read_only=True,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Checkin
|
model = Checkin
|
||||||
fields = ('id', 'datetime', 'list', 'auto_checked_in', 'gate', 'device', 'type')
|
fields = ('id', 'datetime', 'list', 'auto_checked_in', 'gate', 'device', 'device_id', 'type')
|
||||||
|
|
||||||
|
|
||||||
class FailedCheckinSerializer(I18nAwareModelSerializer):
|
class FailedCheckinSerializer(I18nAwareModelSerializer):
|
||||||
@@ -564,6 +569,8 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
|||||||
attendee_name = AttendeeNameField(source='*')
|
attendee_name = AttendeeNameField(source='*')
|
||||||
attendee_name_parts = AttendeeNamePartsField(source='*')
|
attendee_name_parts = AttendeeNamePartsField(source='*')
|
||||||
order__status = serializers.SlugRelatedField(read_only=True, slug_field='status', source='order')
|
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:
|
class Meta:
|
||||||
model = OrderPosition
|
model = OrderPosition
|
||||||
@@ -571,7 +578,8 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
|||||||
'company', 'street', 'zipcode', 'city', 'country', 'state',
|
'company', 'street', 'zipcode', 'city', 'country', 'state',
|
||||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'require_attention',
|
'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):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -583,7 +591,7 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
|||||||
self.fields['item'] = ItemSerializer(read_only=True, context=self.context)
|
self.fields['item'] = ItemSerializer(read_only=True, context=self.context)
|
||||||
|
|
||||||
if 'variation' in self.context['expand']:
|
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']:
|
if 'answers.question' in self.context['expand']:
|
||||||
self.fields['answers'].child.fields['question'] = QuestionSerializer(read_only=True)
|
self.fields['answers'].child.fields['question'] = QuestionSerializer(read_only=True)
|
||||||
@@ -711,6 +719,11 @@ class OrderSerializer(I18nAwareModelSerializer):
|
|||||||
payment_provider = OrderPaymentTypeField(source='*', read_only=True)
|
payment_provider = OrderPaymentTypeField(source='*', read_only=True)
|
||||||
url = OrderURLField(source='*', read_only=True)
|
url = OrderURLField(source='*', read_only=True)
|
||||||
customer = serializers.SlugRelatedField(slug_field='identifier', 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:
|
class Meta:
|
||||||
model = Order
|
model = Order
|
||||||
@@ -719,7 +732,7 @@ class OrderSerializer(I18nAwareModelSerializer):
|
|||||||
'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
|
'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
|
||||||
'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads',
|
'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads',
|
||||||
'checkin_attention', 'checkin_text', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
|
'checkin_attention', 'checkin_text', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
|
||||||
'url', 'customer', 'valid_if_pending'
|
'url', 'customer', 'valid_if_pending', 'api_meta'
|
||||||
)
|
)
|
||||||
read_only_fields = (
|
read_only_fields = (
|
||||||
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
|
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
|
||||||
@@ -729,6 +742,10 @@ class OrderSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*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']:
|
if not self.context['pdf_data']:
|
||||||
self.fields['positions'].child.fields.pop('pdf_data', None)
|
self.fields['positions'].child.fields.pop('pdf_data', None)
|
||||||
|
|
||||||
@@ -775,7 +792,7 @@ class OrderSerializer(I18nAwareModelSerializer):
|
|||||||
# Even though all fields that shouldn't be edited are marked as read_only in the serializer
|
# Even though all fields that shouldn't be edited are marked as read_only in the serializer
|
||||||
# (hopefully), we'll be extra careful here and be explicit about the model fields we update.
|
# (hopefully), we'll be extra careful here and be explicit about the model fields we update.
|
||||||
update_fields = ['comment', 'custom_followup_at', 'checkin_attention', 'checkin_text', 'email', 'locale',
|
update_fields = ['comment', 'custom_followup_at', 'checkin_attention', 'checkin_text', 'email', 'locale',
|
||||||
'phone', 'valid_if_pending']
|
'phone', 'valid_if_pending', 'api_meta']
|
||||||
|
|
||||||
if 'invoice_address' in validated_data:
|
if 'invoice_address' in validated_data:
|
||||||
iadata = validated_data.pop('invoice_address')
|
iadata = validated_data.pop('invoice_address')
|
||||||
@@ -1030,19 +1047,25 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
require_approval = serializers.BooleanField(default=False, required=False)
|
require_approval = serializers.BooleanField(default=False, required=False)
|
||||||
simulate = 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)
|
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):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['positions'].child.fields['voucher'].queryset = self.context['event'].vouchers.all()
|
self.fields['positions'].child.fields['voucher'].queryset = self.context['event'].vouchers.all()
|
||||||
self.fields['customer'].queryset = self.context['event'].organizer.customers.all()
|
self.fields['customer'].queryset = self.context['event'].organizer.customers.all()
|
||||||
self.fields['expires'].required = False
|
self.fields['expires'].required = False
|
||||||
|
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Order
|
model = Order
|
||||||
fields = ('code', 'status', 'testmode', 'email', 'phone', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
|
fields = ('code', 'status', 'testmode', 'email', 'phone', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
|
||||||
'invoice_address', 'positions', 'checkin_attention', 'checkin_text', 'payment_info', 'payment_date',
|
'invoice_address', 'positions', 'checkin_attention', 'checkin_text', 'payment_info', 'payment_date',
|
||||||
'consume_carts', 'force', 'send_email', 'simulate', 'customer', 'custom_followup_at',
|
'consume_carts', 'force', 'send_email', 'simulate', 'customer', 'custom_followup_at',
|
||||||
'require_approval', 'valid_if_pending', 'expires')
|
'require_approval', 'valid_if_pending', 'expires', 'api_meta')
|
||||||
|
|
||||||
def validate_payment_provider(self, pp):
|
def validate_payment_provider(self, pp):
|
||||||
if pp is None:
|
if pp is None:
|
||||||
@@ -1056,11 +1079,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
raise ValidationError('Expiration date must be in the future.')
|
raise ValidationError('Expiration date must be in the future.')
|
||||||
return expires
|
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):
|
def validate_code(self, code):
|
||||||
if code and Order.objects.filter(event__organizer=self.context['event'].organizer, code=code).exists():
|
if code and Order.objects.filter(event__organizer=self.context['event'].organizer, code=code).exists():
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
@@ -1122,20 +1140,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
raise ValidationError(errs)
|
raise ValidationError(errs)
|
||||||
return data
|
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):
|
def create(self, validated_data):
|
||||||
fees_data = validated_data.pop('fees') if 'fees' in validated_data else []
|
fees_data = validated_data.pop('fees') if 'fees' in validated_data else []
|
||||||
positions_data = validated_data.pop('positions') if 'positions' in validated_data else []
|
positions_data = validated_data.pop('positions') if 'positions' in validated_data else []
|
||||||
@@ -1144,9 +1148,16 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
payment_date = validated_data.pop('payment_date', now())
|
payment_date = validated_data.pop('payment_date', now())
|
||||||
force = validated_data.pop('force', False)
|
force = validated_data.pop('force', False)
|
||||||
simulate = validated_data.pop('simulate', False)
|
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)
|
self._send_mail = validated_data.pop('send_email', False)
|
||||||
if self._send_mail is None:
|
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:
|
if 'invoice_address' in validated_data:
|
||||||
iadata = validated_data.pop('invoice_address')
|
iadata = validated_data.pop('invoice_address')
|
||||||
@@ -1306,7 +1317,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
errs[i]['seat'] = ['The specified seat does not exist.']
|
errs[i]['seat'] = ['The specified seat does not exist.']
|
||||||
else:
|
else:
|
||||||
seat_usage[seat] += 1
|
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)]
|
errs[i]['seat'] = [gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)]
|
||||||
elif seated:
|
elif seated:
|
||||||
errs[i]['seat'] = ['The specified product requires to choose a seat.']
|
errs[i]['seat'] = ['The specified product requires to choose a seat.']
|
||||||
@@ -1315,7 +1327,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
if 'valid_from' not in pos_data and 'valid_until' not in pos_data:
|
if 'valid_from' not in pos_data and 'valid_until' not in pos_data:
|
||||||
valid_from, valid_until = pos_data['item'].compute_validity(
|
valid_from, valid_until = pos_data['item'].compute_validity(
|
||||||
requested_start=(
|
requested_start=(
|
||||||
max(requested_valid_from, now())
|
requested_valid_from
|
||||||
if requested_valid_from and pos_data['item'].validity_dynamic_start_choice
|
if requested_valid_from and pos_data['item'].validity_dynamic_start_choice
|
||||||
else now()
|
else now()
|
||||||
),
|
),
|
||||||
@@ -1365,6 +1377,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
if validated_data.get('locale', None) is None:
|
if validated_data.get('locale', None) is None:
|
||||||
validated_data['locale'] = self.context['event'].settings.locale
|
validated_data['locale'] = self.context['event'].settings.locale
|
||||||
|
|
||||||
order = Order(event=self.context['event'], **validated_data)
|
order = Order(event=self.context['event'], **validated_data)
|
||||||
if not validated_data.get('expires'):
|
if not validated_data.get('expires'):
|
||||||
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
|
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
|
||||||
@@ -1439,6 +1452,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
if not pos.item.tax_rule or pos.item.tax_rule.price_includes_tax:
|
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))
|
price_after_voucher = max(pos.price, pos.voucher.calculate_price(listed_price))
|
||||||
else:
|
else:
|
||||||
|
pos._calculate_tax(invoice_address=ia)
|
||||||
price_after_voucher = max(pos.price - pos.tax_value, pos.voucher.calculate_price(listed_price))
|
price_after_voucher = max(pos.price - pos.tax_value, pos.voucher.calculate_price(listed_price))
|
||||||
else:
|
else:
|
||||||
price_after_voucher = listed_price
|
price_after_voucher = listed_price
|
||||||
@@ -1466,7 +1480,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
answers_data = pos_data.pop('answers', [])
|
answers_data = pos_data.pop('answers', [])
|
||||||
use_reusable_medium = pos_data.pop('use_reusable_medium', None)
|
use_reusable_medium = pos_data.pop('use_reusable_medium', None)
|
||||||
pos = pos_data['__instance']
|
pos = pos_data['__instance']
|
||||||
pos._calculate_tax()
|
pos._calculate_tax(invoice_address=ia)
|
||||||
|
|
||||||
if simulate:
|
if simulate:
|
||||||
pos = WrappedModel(pos)
|
pos = WrappedModel(pos)
|
||||||
@@ -1585,7 +1599,10 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
if order.total == Decimal('0.00') and validated_data.get('status') == Order.STATUS_PAID and not payment_provider:
|
if order.total == Decimal('0.00') and validated_data.get('status') == Order.STATUS_PAID and not payment_provider:
|
||||||
payment_provider = 'free'
|
payment_provider = 'free'
|
||||||
|
|
||||||
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.status = Order.STATUS_PAID
|
||||||
order.save()
|
order.save()
|
||||||
order.payments.create(
|
order.payments.create(
|
||||||
@@ -1597,6 +1614,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
elif validated_data.get('status') == Order.STATUS_PAID:
|
elif validated_data.get('status') == Order.STATUS_PAID:
|
||||||
if not payment_provider:
|
if not payment_provider:
|
||||||
raise ValidationError('You cannot create a paid order without a 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(
|
order.payments.create(
|
||||||
amount=order.total,
|
amount=order.total,
|
||||||
provider=payment_provider,
|
provider=payment_provider,
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ from pretix.base.i18n import get_language_without_region
|
|||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Customer, Device, GiftCard, GiftCardAcceptance, GiftCardTransaction,
|
Customer, Device, GiftCard, GiftCardAcceptance, GiftCardTransaction,
|
||||||
Membership, MembershipType, OrderPosition, Organizer, ReusableMedium,
|
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.models.seating import SeatingPlanLayoutValidator
|
||||||
from pretix.base.services.mail import SendMailException, mail
|
from pretix.base.services.mail import SendMailException, mail
|
||||||
@@ -79,8 +79,8 @@ class CustomerSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Customer
|
model = Customer
|
||||||
fields = ('identifier', 'external_identifier', 'email', 'name', 'name_parts', 'is_active', 'is_verified', 'last_login', 'date_joined',
|
fields = ('identifier', 'external_identifier', 'email', 'phone', 'name', 'name_parts', 'is_active',
|
||||||
'locale', 'last_modified', 'notes')
|
'is_verified', 'last_login', 'date_joined', 'locale', 'last_modified', 'notes')
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
if instance and instance.provider_id:
|
if instance and instance.provider_id:
|
||||||
@@ -165,6 +165,36 @@ class FlexibleTicketRelatedField(serializers.PrimaryKeyRelatedField):
|
|||||||
self.fail('incorrect_type', data_type=type(data).__name__)
|
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):
|
class GiftCardSerializer(I18nAwareModelSerializer):
|
||||||
value = serializers.DecimalField(max_digits=13, decimal_places=2, min_value=Decimal('0.00'))
|
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())
|
owner_ticket = FlexibleTicketRelatedField(required=False, allow_null=True, queryset=OrderPosition.all.none())
|
||||||
@@ -239,7 +269,7 @@ class TeamSerializer(serializers.ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Team
|
model = Team
|
||||||
fields = (
|
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_organizer_settings', 'can_manage_gift_cards', 'can_change_event_settings',
|
||||||
'can_change_items', 'can_view_orders', 'can_change_orders', 'can_view_vouchers',
|
'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'
|
'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
|
except OSError: # pragma: no cover
|
||||||
logger.error('Deleting file %s failed.' % fname.name)
|
logger.error('Deleting file %s failed.' % fname.name)
|
||||||
instance.delete(attr)
|
instance.delete(attr)
|
||||||
|
self.changed_data.append(attr)
|
||||||
else:
|
else:
|
||||||
# file is unchanged
|
# file is unchanged
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ orga_router.register(r'webhooks', webhooks.WebHookViewSet)
|
|||||||
orga_router.register(r'seatingplans', organizer.SeatingPlanViewSet)
|
orga_router.register(r'seatingplans', organizer.SeatingPlanViewSet)
|
||||||
orga_router.register(r'giftcards', organizer.GiftCardViewSet)
|
orga_router.register(r'giftcards', organizer.GiftCardViewSet)
|
||||||
orga_router.register(r'customers', organizer.CustomerViewSet)
|
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'memberships', organizer.MembershipViewSet)
|
||||||
orga_router.register(r'membershiptypes', organizer.MembershipTypeViewSet)
|
orga_router.register(r'membershiptypes', organizer.MembershipTypeViewSet)
|
||||||
orga_router.register(r'reusablemedia', media.ReusableMediaViewSet)
|
orga_router.register(r'reusablemedia', media.ReusableMediaViewSet)
|
||||||
@@ -86,6 +87,7 @@ event_router.register(r'invoices', order.InvoiceViewSet)
|
|||||||
event_router.register(r'revokedsecrets', order.RevokedSecretViewSet, basename='revokedsecrets')
|
event_router.register(r'revokedsecrets', order.RevokedSecretViewSet, basename='revokedsecrets')
|
||||||
event_router.register(r'blockedsecrets', order.BlockedSecretViewSet, basename='blockedsecrets')
|
event_router.register(r'blockedsecrets', order.BlockedSecretViewSet, basename='blockedsecrets')
|
||||||
event_router.register(r'taxrules', event.TaxRuleViewSet)
|
event_router.register(r'taxrules', event.TaxRuleViewSet)
|
||||||
|
event_router.register(r'seats', event.SeatViewSet)
|
||||||
event_router.register(r'waitinglistentries', waitinglist.WaitingListViewSet)
|
event_router.register(r'waitinglistentries', waitinglist.WaitingListViewSet)
|
||||||
event_router.register(r'checkinlists', checkin.CheckinListViewSet)
|
event_router.register(r'checkinlists', checkin.CheckinListViewSet)
|
||||||
event_router.register(r'cartpositions', cart.CartPositionViewSet)
|
event_router.register(r'cartpositions', cart.CartPositionViewSet)
|
||||||
@@ -94,6 +96,9 @@ event_router.register(r'exporters', exporters.EventExportersViewSet, basename='e
|
|||||||
event_router.register(r'shredders', shredders.EventShreddersViewSet, basename='shredders')
|
event_router.register(r'shredders', shredders.EventShreddersViewSet, basename='shredders')
|
||||||
event_router.register(r'item_meta_properties', event.ItemMetaPropertiesViewSet)
|
event_router.register(r'item_meta_properties', event.ItemMetaPropertiesViewSet)
|
||||||
|
|
||||||
|
subevent_router = routers.DefaultRouter()
|
||||||
|
subevent_router.register(r'seats', event.SeatViewSet)
|
||||||
|
|
||||||
checkinlist_router = routers.DefaultRouter()
|
checkinlist_router = routers.DefaultRouter()
|
||||||
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet, basename='checkinlistpos')
|
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet, basename='checkinlistpos')
|
||||||
|
|
||||||
@@ -131,6 +136,7 @@ urlpatterns = [
|
|||||||
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/settings/$', event.EventSettingsView.as_view(),
|
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/settings/$', event.EventSettingsView.as_view(),
|
||||||
name="event.settings"),
|
name="event.settings"),
|
||||||
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/', include(event_router.urls)),
|
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/', include(event_router.urls)),
|
||||||
|
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/subevents/(?P<subevent>\d+)/', include(subevent_router.urls)),
|
||||||
re_path(r'^organizers/(?P<organizer>[^/]+)/teams/(?P<team>[^/]+)/', include(team_router.urls)),
|
re_path(r'^organizers/(?P<organizer>[^/]+)/teams/(?P<team>[^/]+)/', include(team_router.urls)),
|
||||||
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/items/(?P<item>[^/]+)/', include(item_router.urls)),
|
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/items/(?P<item>[^/]+)/', include(item_router.urls)),
|
||||||
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/questions/(?P<question>[^/]+)/',
|
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/questions/(?P<question>[^/]+)/',
|
||||||
|
|||||||
@@ -211,8 +211,12 @@ class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnly
|
|||||||
|
|
||||||
if validated_data.get('seat'):
|
if validated_data.get('seat'):
|
||||||
# Assumption: Add-ons currently can't have seats, thus we only need to check the main product
|
# 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(
|
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'],
|
distance_ignore_cart_id=validated_data['cart_id'],
|
||||||
ignore_voucher_id=validated_data['voucher'].pk if validated_data.get('voucher') else None,
|
ignore_voucher_id=validated_data['voucher'].pk if validated_data.get('voucher') else None,
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
|||||||
if 'subevent' in self.request.query_params.getlist('expand'):
|
if 'subevent' in self.request.query_params.getlist('expand'):
|
||||||
qs = qs.prefetch_related(
|
qs = qs.prefetch_related(
|
||||||
'subevent', 'subevent__event', 'subevent__subeventitem_set', 'subevent__subeventitemvariation_set',
|
'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
|
return qs
|
||||||
|
|
||||||
@@ -377,7 +377,7 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
|
|||||||
Prefetch(
|
Prefetch(
|
||||||
'positions',
|
'positions',
|
||||||
OrderPosition.objects.prefetch_related(
|
OrderPosition.objects.prefetch_related(
|
||||||
Prefetch('checkins', queryset=Checkin.objects.all()),
|
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
||||||
'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -406,7 +406,7 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
|
|||||||
'item__variations').select_related('item__tax_rule')
|
'item__variations').select_related('item__tax_rule')
|
||||||
|
|
||||||
if expand and 'variation' in expand:
|
if expand and 'variation' in expand:
|
||||||
qs = qs.prefetch_related('variation')
|
qs = qs.prefetch_related('variation', 'variation__meta_values')
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|||||||
@@ -200,6 +200,11 @@ class UpdateView(APIView):
|
|||||||
device.save()
|
device.save()
|
||||||
device.log_action('pretix.device.updated', data=serializer.validated_data, auth=device)
|
device.log_action('pretix.device.updated', data=serializer.validated_data, auth=device)
|
||||||
|
|
||||||
|
from ...base.signals import device_info_updated
|
||||||
|
device_info_updated.send(
|
||||||
|
sender=Device, old_device=request.auth, new_device=device
|
||||||
|
)
|
||||||
|
|
||||||
serializer = DeviceSerializer(device)
|
serializer = DeviceSerializer(device)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,9 @@ class DiscountViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
write_permission = 'can_change_items'
|
write_permission = 'can_change_items'
|
||||||
|
|
||||||
def get_queryset(self):
|
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):
|
def perform_create(self, serializer):
|
||||||
serializer.save(event=self.request.event)
|
serializer.save(event=self.request.event)
|
||||||
|
|||||||
@@ -40,27 +40,28 @@ from django.utils.timezone import now
|
|||||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
from rest_framework import serializers, views, viewsets
|
from rest_framework import serializers, views, viewsets
|
||||||
from rest_framework.exceptions import PermissionDenied, ValidationError
|
from rest_framework.exceptions import (
|
||||||
|
NotFound, PermissionDenied, ValidationError,
|
||||||
|
)
|
||||||
|
from rest_framework.generics import get_object_or_404
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from pretix.api.auth.permission import EventCRUDPermission
|
from pretix.api.auth.permission import EventCRUDPermission
|
||||||
from pretix.api.pagination import TotalOrderingFilter
|
from pretix.api.pagination import TotalOrderingFilter
|
||||||
from pretix.api.serializers.event import (
|
from pretix.api.serializers.event import (
|
||||||
CloneEventSerializer, DeviceEventSettingsSerializer, EventSerializer,
|
CloneEventSerializer, DeviceEventSettingsSerializer, EventSerializer,
|
||||||
EventSettingsSerializer, ItemMetaPropertiesSerializer, SubEventSerializer,
|
EventSettingsSerializer, ItemMetaPropertiesSerializer, SeatSerializer,
|
||||||
TaxRuleSerializer,
|
SubEventSerializer, TaxRuleSerializer,
|
||||||
)
|
)
|
||||||
from pretix.api.views import ConditionalListView
|
from pretix.api.views import ConditionalListView
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CartPosition, Device, Event, ItemMetaProperty, SeatCategoryMapping,
|
CartPosition, Device, Event, ItemMetaProperty, Seat, SeatCategoryMapping,
|
||||||
TaxRule, TeamAPIToken,
|
TaxRule, TeamAPIToken,
|
||||||
)
|
)
|
||||||
from pretix.base.models.event import SubEvent
|
from pretix.base.models.event import SubEvent
|
||||||
from pretix.base.services.quotas import QuotaAvailability
|
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.dicts import merge_dicts
|
||||||
from pretix.helpers.i18n import i18ncomp
|
from pretix.helpers.i18n import i18ncomp
|
||||||
from pretix.presale.style import regenerate_css
|
|
||||||
from pretix.presale.views.organizer import filter_qs_by_attr
|
from pretix.presale.views.organizer import filter_qs_by_attr
|
||||||
|
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
@@ -115,7 +116,10 @@ with scopes_disabled():
|
|||||||
return queryset.exclude(expr)
|
return queryset.exclude(expr)
|
||||||
|
|
||||||
def sales_channel_qs(self, queryset, name, value):
|
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):
|
def search_qs(self, queryset, name, value):
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
@@ -137,6 +141,12 @@ class EventViewSet(viewsets.ModelViewSet):
|
|||||||
ordering_fields = ('date_from', 'slug')
|
ordering_fields = ('date_from', 'slug')
|
||||||
filterset_class = EventFilter
|
filterset_class = EventFilter
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
return {
|
||||||
|
**super().get_serializer_context(),
|
||||||
|
"organizer": self.request.organizer,
|
||||||
|
}
|
||||||
|
|
||||||
def get_copy_from_queryset(self):
|
def get_copy_from_queryset(self):
|
||||||
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
||||||
return self.request.auth.get_events_with_any_permission()
|
return self.request.auth.get_events_with_any_permission()
|
||||||
@@ -155,13 +165,20 @@ class EventViewSet(viewsets.ModelViewSet):
|
|||||||
qs = filter_qs_by_attr(qs, self.request)
|
qs = filter_qs_by_attr(qs, self.request)
|
||||||
|
|
||||||
if 'with_availability_for' in self.request.GET:
|
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(
|
return qs.prefetch_related(
|
||||||
'organizer',
|
'organizer',
|
||||||
'meta_values',
|
'meta_values',
|
||||||
'meta_values__property',
|
'meta_values__property',
|
||||||
'item_meta_properties',
|
'item_meta_properties',
|
||||||
|
'limit_sales_channels',
|
||||||
Prefetch(
|
Prefetch(
|
||||||
'seat_category_mappings',
|
'seat_category_mappings',
|
||||||
to_attr='_seat_category_mappings',
|
to_attr='_seat_category_mappings',
|
||||||
@@ -190,7 +207,10 @@ class EventViewSet(viewsets.ModelViewSet):
|
|||||||
serializer = self.get_serializer(page, many=True)
|
serializer = self.get_serializer(page, many=True)
|
||||||
return self.get_paginated_response(serializer.data)
|
return self.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
|
@transaction.atomic()
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
|
original_data = self.get_serializer(instance=serializer.instance).data
|
||||||
|
|
||||||
current_live_value = serializer.instance.live
|
current_live_value = serializer.instance.live
|
||||||
updated_live_value = serializer.validated_data.get('live', None)
|
updated_live_value = serializer.validated_data.get('live', None)
|
||||||
current_plugins_value = serializer.instance.get_plugins()
|
current_plugins_value = serializer.instance.get_plugins()
|
||||||
@@ -198,6 +218,11 @@ class EventViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
super().perform_update(serializer)
|
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:
|
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'
|
log_action = 'pretix.event.live.activated' if updated_live_value else 'pretix.event.live.deactivated'
|
||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
@@ -262,8 +287,6 @@ class EventViewSet(viewsets.ModelViewSet):
|
|||||||
new_event.is_public = serializer.validated_data['is_public']
|
new_event.is_public = serializer.validated_data['is_public']
|
||||||
if 'testmode' in serializer.validated_data:
|
if 'testmode' in serializer.validated_data:
|
||||||
new_event.testmode = serializer.validated_data['testmode']
|
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:
|
if 'has_subevents' in serializer.validated_data:
|
||||||
new_event.has_subevents = serializer.validated_data['has_subevents']
|
new_event.has_subevents = serializer.validated_data['has_subevents']
|
||||||
if 'date_admission' in serializer.validated_data:
|
if 'date_admission' in serializer.validated_data:
|
||||||
@@ -271,6 +294,11 @@ class EventViewSet(viewsets.ModelViewSet):
|
|||||||
new_event.save()
|
new_event.save()
|
||||||
if 'timezone' in serializer.validated_data:
|
if 'timezone' in serializer.validated_data:
|
||||||
new_event.settings.timezone = serializer.validated_data['timezone']
|
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']
|
||||||
|
if not new_event.all_sales_channels:
|
||||||
|
new_event.limit_sales_channels.set(serializer.validated_data['limit_sales_channels'])
|
||||||
else:
|
else:
|
||||||
serializer.instance.set_defaults()
|
serializer.instance.set_defaults()
|
||||||
|
|
||||||
@@ -343,7 +371,7 @@ with scopes_disabled():
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SubEvent
|
model = SubEvent
|
||||||
fields = ['active', 'event__live']
|
fields = ['is_public', 'active', 'event__live']
|
||||||
|
|
||||||
def ends_after_qs(self, queryset, name, value):
|
def ends_after_qs(self, queryset, name, value):
|
||||||
expr = Q(
|
expr = Q(
|
||||||
@@ -373,7 +401,10 @@ with scopes_disabled():
|
|||||||
return queryset.exclude(expr)
|
return queryset.exclude(expr)
|
||||||
|
|
||||||
def sales_channel_qs(self, queryset, name, value):
|
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):
|
def search_qs(self, queryset, name, value):
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
@@ -415,13 +446,19 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
elif self.request.user.is_authenticated:
|
elif self.request.user.is_authenticated:
|
||||||
qs = SubEvent.objects.filter(
|
qs = SubEvent.objects.filter(
|
||||||
event__organizer=self.request.organizer,
|
event__organizer=self.request.organizer,
|
||||||
event__in=self.request.user.get_events_with_any_permission()
|
event__in=self.request.user.get_events_with_any_permission(request=self.request)
|
||||||
)
|
)
|
||||||
|
|
||||||
qs = filter_qs_by_attr(qs, self.request)
|
qs = filter_qs_by_attr(qs, self.request)
|
||||||
|
|
||||||
if 'with_availability_for' in self.request.GET:
|
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(
|
return qs.prefetch_related(
|
||||||
'event',
|
'event',
|
||||||
@@ -622,15 +659,88 @@ class EventSettingsView(views.APIView):
|
|||||||
s.is_valid(raise_exception=True)
|
s.is_valid(raise_exception=True)
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
s.save()
|
s.save()
|
||||||
self.request.event.log_action(
|
if s.changed_data:
|
||||||
'pretix.event.settings', user=self.request.user, auth=self.request.auth, data={
|
self.request.event.log_action(
|
||||||
k: v for k, v in s.validated_data.items()
|
'pretix.event.settings', user=self.request.user, auth=self.request.auth, data={
|
||||||
}
|
k: v for k, v in s.validated_data.items()
|
||||||
)
|
}
|
||||||
if any(p in s.changed_data for p in SETTINGS_AFFECTING_CSS):
|
)
|
||||||
regenerate_css.apply_async(args=(request.event.pk,))
|
|
||||||
s = EventSettingsSerializer(
|
s = EventSettingsSerializer(
|
||||||
instance=request.event.settings, event=request.event, context={
|
instance=request.event.settings, event=request.event, context={
|
||||||
'request': request
|
'request': request
|
||||||
})
|
})
|
||||||
return Response(s.data)
|
return Response(s.data)
|
||||||
|
|
||||||
|
|
||||||
|
class SeatFilter(FilterSet):
|
||||||
|
is_available = django_filters.BooleanFilter(method="is_available_qs")
|
||||||
|
|
||||||
|
def is_available_qs(self, queryset, name, value):
|
||||||
|
expr = (
|
||||||
|
Q(orderposition_id__isnull=True, cartposition_id__isnull=True, voucher_id__isnull=True)
|
||||||
|
)
|
||||||
|
if self.request.event.settings.seating_minimal_distance:
|
||||||
|
expr = expr & Q(has_closeby_taken=False)
|
||||||
|
if value:
|
||||||
|
return queryset.filter(expr)
|
||||||
|
else:
|
||||||
|
return queryset.exclude(expr)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Seat
|
||||||
|
fields = ('zone_name', 'row_name', 'row_label', 'seat_number', 'seat_label', 'seat_guid', 'blocked',)
|
||||||
|
|
||||||
|
|
||||||
|
class SeatViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||||
|
serializer_class = SeatSerializer
|
||||||
|
queryset = Seat.objects.none()
|
||||||
|
write_permission = 'can_change_event_settings'
|
||||||
|
filter_backends = (DjangoFilterBackend, )
|
||||||
|
filterset_class = SeatFilter
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
if self.request.event.has_subevents and 'subevent' in self.request.resolver_match.kwargs:
|
||||||
|
try:
|
||||||
|
subevent = self.request.event.subevents.get(pk=self.request.resolver_match.kwargs['subevent'])
|
||||||
|
except SubEvent.DoesNotExist:
|
||||||
|
raise NotFound('Subevent not found')
|
||||||
|
qs = Seat.annotated(
|
||||||
|
event_id=self.request.event.id,
|
||||||
|
subevent=subevent,
|
||||||
|
qs=subevent.seats.all(),
|
||||||
|
annotate_ids=True,
|
||||||
|
minimal_distance=self.request.event.settings.seating_minimal_distance,
|
||||||
|
distance_only_within_row=self.request.event.settings.seating_distance_only_within_row,
|
||||||
|
)
|
||||||
|
elif not self.request.event.has_subevents and 'subevent' not in self.request.resolver_match.kwargs:
|
||||||
|
qs = Seat.annotated(
|
||||||
|
event_id=self.request.event.id,
|
||||||
|
subevent=None,
|
||||||
|
qs=self.request.event.seats.all(),
|
||||||
|
annotate_ids=True,
|
||||||
|
minimal_distance=self.request.event.settings.seating_minimal_distance,
|
||||||
|
distance_only_within_row=self.request.event.settings.seating_distance_only_within_row,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise NotFound('Please use the subevent-specific endpoint' if self.request.event.has_subevents
|
||||||
|
else 'This event has no subevents')
|
||||||
|
|
||||||
|
return qs
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
ctx = super().get_serializer_context()
|
||||||
|
ctx['expand_fields'] = self.request.query_params.getlist('expand')
|
||||||
|
ctx['order_context'] = {
|
||||||
|
'event': self.request.event,
|
||||||
|
'pdf_data': None,
|
||||||
|
}
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
def perform_update(self, serializer):
|
||||||
|
super().perform_update(serializer)
|
||||||
|
serializer.instance.event.log_action(
|
||||||
|
"pretix.event.seats.blocks.changed",
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
data={"seats": [serializer.instance.pk]},
|
||||||
|
)
|
||||||
|
|||||||
@@ -56,10 +56,17 @@ from pretix.base.models import (
|
|||||||
)
|
)
|
||||||
from pretix.base.services.quotas import QuotaAvailability
|
from pretix.base.services.quotas import QuotaAvailability
|
||||||
from pretix.helpers.dicts import merge_dicts
|
from pretix.helpers.dicts import merge_dicts
|
||||||
|
from pretix.helpers.i18n import i18ncomp
|
||||||
|
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
class ItemFilter(FilterSet):
|
class ItemFilter(FilterSet):
|
||||||
tax_rate = django_filters.CharFilter(method='tax_rate_qs')
|
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):
|
def tax_rate_qs(self, queryset, name, value):
|
||||||
if value in ("0", "None", "0.00"):
|
if value in ("0", "None", "0.00"):
|
||||||
@@ -71,6 +78,18 @@ with scopes_disabled():
|
|||||||
model = Item
|
model = Item
|
||||||
fields = ['active', 'category', 'admission', 'tax_rate', 'free_price']
|
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):
|
class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||||
serializer_class = ItemSerializer
|
serializer_class = ItemSerializer
|
||||||
@@ -87,6 +106,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
'variations', 'addons', 'bundles', 'meta_values', 'meta_values__property',
|
'variations', 'addons', 'bundles', 'meta_values', 'meta_values__property',
|
||||||
'variations__meta_values', 'variations__meta_values__property',
|
'variations__meta_values', 'variations__meta_values__property',
|
||||||
'require_membership_types', 'variations__require_membership_types',
|
'require_membership_types', 'variations__require_membership_types',
|
||||||
|
'limit_sales_channels', 'variations__limit_sales_channels',
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
@@ -139,6 +159,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
|||||||
serializer_class = ItemVariationSerializer
|
serializer_class = ItemVariationSerializer
|
||||||
queryset = ItemVariation.objects.none()
|
queryset = ItemVariation.objects.none()
|
||||||
filter_backends = (DjangoFilterBackend, TotalOrderingFilter,)
|
filter_backends = (DjangoFilterBackend, TotalOrderingFilter,)
|
||||||
|
filterset_class = ItemVariationFilter
|
||||||
ordering_fields = ('id', 'position')
|
ordering_fields = ('id', 'position')
|
||||||
ordering = ('id',)
|
ordering = ('id',)
|
||||||
permission = None
|
permission = None
|
||||||
@@ -152,7 +173,8 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
|||||||
return self.item.variations.all().prefetch_related(
|
return self.item.variations.all().prefetch_related(
|
||||||
'meta_values',
|
'meta_values',
|
||||||
'meta_values__property',
|
'meta_values__property',
|
||||||
'require_membership_types'
|
'require_membership_types',
|
||||||
|
'limit_sales_channels',
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
|
|||||||
queryset=OrderPosition.objects.select_related(
|
queryset=OrderPosition.objects.select_related(
|
||||||
'order', 'order__event', 'order__event__organizer', 'seat',
|
'order', 'order__event', 'order__event__organizer', 'seat',
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
Prefetch('checkins', queryset=Checkin.objects.all()),
|
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
||||||
'answers', 'answers__options', 'answers__question',
|
'answers', 'answers__options', 'answers__question',
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import datetime
|
import datetime
|
||||||
|
import logging
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
@@ -27,7 +28,7 @@ from zoneinfo import ZoneInfo
|
|||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import transaction
|
from django.db import IntegrityError, transaction
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Exists, F, OuterRef, Prefetch, Q, Subquery, prefetch_related_objects,
|
Exists, F, OuterRef, Prefetch, Q, Subquery, prefetch_related_objects,
|
||||||
)
|
)
|
||||||
@@ -48,6 +49,7 @@ from rest_framework.mixins import CreateModelMixin
|
|||||||
from rest_framework.permissions import SAFE_METHODS
|
from rest_framework.permissions import SAFE_METHODS
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from pretix.api.filters import MultipleCharFilter
|
||||||
from pretix.api.models import OAuthAccessToken
|
from pretix.api.models import OAuthAccessToken
|
||||||
from pretix.api.pagination import TotalOrderingFilter
|
from pretix.api.pagination import TotalOrderingFilter
|
||||||
from pretix.api.serializers.order import (
|
from pretix.api.serializers.order import (
|
||||||
@@ -96,6 +98,9 @@ from pretix.base.signals import (
|
|||||||
)
|
)
|
||||||
from pretix.base.templatetags.money import money_filter
|
from pretix.base.templatetags.money import money_filter
|
||||||
from pretix.control.signals import order_search_filter_q
|
from pretix.control.signals import order_search_filter_q
|
||||||
|
from pretix.helpers import OF_SELF
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
class OrderFilter(FilterSet):
|
class OrderFilter(FilterSet):
|
||||||
@@ -104,6 +109,7 @@ with scopes_disabled():
|
|||||||
status = django_filters.CharFilter(field_name='status', lookup_expr='iexact')
|
status = django_filters.CharFilter(field_name='status', lookup_expr='iexact')
|
||||||
modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte')
|
modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte')
|
||||||
created_since = django_filters.IsoDateTimeFilter(field_name='datetime', 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_after = django_filters.IsoDateTimeFilter(method='subevent_after_qs')
|
||||||
subevent_before = django_filters.IsoDateTimeFilter(method='subevent_before_qs')
|
subevent_before = django_filters.IsoDateTimeFilter(method='subevent_before_qs')
|
||||||
search = django_filters.CharFilter(method='search_qs')
|
search = django_filters.CharFilter(method='search_qs')
|
||||||
@@ -111,6 +117,8 @@ with scopes_disabled():
|
|||||||
variation = django_filters.CharFilter(field_name='all_positions', lookup_expr='variation_id', distinct=True)
|
variation = django_filters.CharFilter(field_name='all_positions', lookup_expr='variation_id', distinct=True)
|
||||||
subevent = django_filters.CharFilter(field_name='all_positions', lookup_expr='subevent_id', distinct=True)
|
subevent = django_filters.CharFilter(field_name='all_positions', lookup_expr='subevent_id', distinct=True)
|
||||||
customer = django_filters.CharFilter(field_name='customer__identifier')
|
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:
|
class Meta:
|
||||||
model = Order
|
model = Order
|
||||||
@@ -134,6 +142,11 @@ with scopes_disabled():
|
|||||||
)
|
)
|
||||||
return qs
|
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):
|
def subevent_before_qs(self, qs, name, value):
|
||||||
if getattr(self.request, 'event', None):
|
if getattr(self.request, 'event', None):
|
||||||
subevents = self.request.event.subevents
|
subevents = self.request.event.subevents
|
||||||
@@ -225,7 +238,7 @@ class OrderViewSetMixin:
|
|||||||
if 'customer' not in self.request.GET.getlist('exclude'):
|
if 'customer' not in self.request.GET.getlist('exclude'):
|
||||||
qs = qs.select_related('customer')
|
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
|
return qs
|
||||||
|
|
||||||
def _positions_prefetch(self, request):
|
def _positions_prefetch(self, request):
|
||||||
@@ -245,7 +258,7 @@ class OrderViewSetMixin:
|
|||||||
return Prefetch(
|
return Prefetch(
|
||||||
'positions',
|
'positions',
|
||||||
opq.all().prefetch_related(
|
opq.all().prefetch_related(
|
||||||
Prefetch('checkins', queryset=Checkin.objects.all()),
|
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
||||||
Prefetch('item', queryset=self.request.event.items.prefetch_related(
|
Prefetch('item', queryset=self.request.event.items.prefetch_related(
|
||||||
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'), to_attr='meta_values_cached')
|
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'), to_attr='meta_values_cached')
|
||||||
)),
|
)),
|
||||||
@@ -266,7 +279,7 @@ class OrderViewSetMixin:
|
|||||||
return Prefetch(
|
return Prefetch(
|
||||||
'positions',
|
'positions',
|
||||||
opq.all().prefetch_related(
|
opq.all().prefetch_related(
|
||||||
Prefetch('checkins', queryset=Checkin.objects.all()),
|
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
||||||
'item', 'variation',
|
'item', 'variation',
|
||||||
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options', 'question').order_by('question__position')),
|
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options', 'question').order_by('question__position')),
|
||||||
'seat',
|
'seat',
|
||||||
@@ -312,6 +325,11 @@ class OrganizerOrderViewSet(OrderViewSetMixin, viewsets.ReadOnlyModelViewSet):
|
|||||||
else:
|
else:
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
ctx = super().get_serializer_context()
|
||||||
|
ctx['organizer'] = self.request.organizer
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
||||||
permission = 'can_view_orders'
|
permission = 'can_view_orders'
|
||||||
@@ -572,8 +590,10 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
|||||||
return self.retrieve(request, [], **kwargs)
|
return self.retrieve(request, [], **kwargs)
|
||||||
|
|
||||||
@action(detail=True, methods=['POST'])
|
@action(detail=True, methods=['POST'])
|
||||||
|
@transaction.atomic()
|
||||||
def create_invoice(self, request, **kwargs):
|
def create_invoice(self, request, **kwargs):
|
||||||
order = self.get_object()
|
order = self.get_object()
|
||||||
|
order = Order.objects.select_for_update(of=OF_SELF).get(pk=order.pk)
|
||||||
has_inv = order.invoices.exists() and not (
|
has_inv = order.invoices.exists() and not (
|
||||||
order.status in (Order.STATUS_PAID, Order.STATUS_PENDING)
|
order.status in (Order.STATUS_PAID, Order.STATUS_PENDING)
|
||||||
and order.invoices.filter(is_cancellation=True).count() >= order.invoices.filter(is_cancellation=False).count()
|
and order.invoices.filter(is_cancellation=True).count() >= order.invoices.filter(is_cancellation=False).count()
|
||||||
@@ -900,7 +920,11 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
|||||||
order_modified.send(sender=serializer.instance.event, order=serializer.instance)
|
order_modified.send(sender=serializer.instance.event, order=serializer.instance)
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
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):
|
def perform_destroy(self, instance):
|
||||||
if not instance.testmode:
|
if not instance.testmode:
|
||||||
@@ -1068,7 +1092,7 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
|
|||||||
'item_meta_properties',
|
'item_meta_properties',
|
||||||
)
|
)
|
||||||
qs = qs.prefetch_related(
|
qs = qs.prefetch_related(
|
||||||
Prefetch('checkins', queryset=Checkin.objects.all()),
|
Prefetch('checkins', queryset=Checkin.objects.select_related("device")),
|
||||||
Prefetch('item', queryset=self.request.event.items.prefetch_related(
|
Prefetch('item', queryset=self.request.event.items.prefetch_related(
|
||||||
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'),
|
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'),
|
||||||
to_attr='meta_values_cached')
|
to_attr='meta_values_cached')
|
||||||
@@ -1087,7 +1111,7 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
|
|||||||
Prefetch(
|
Prefetch(
|
||||||
'positions',
|
'positions',
|
||||||
qs.prefetch_related(
|
qs.prefetch_related(
|
||||||
Prefetch('checkins', queryset=Checkin.objects.all()),
|
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
||||||
Prefetch('item', queryset=self.request.event.items.prefetch_related(
|
Prefetch('item', queryset=self.request.event.items.prefetch_related(
|
||||||
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'),
|
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'),
|
||||||
to_attr='meta_values_cached')
|
to_attr='meta_values_cached')
|
||||||
@@ -1111,7 +1135,7 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
qs = qs.prefetch_related(
|
qs = qs.prefetch_related(
|
||||||
Prefetch('checkins', queryset=Checkin.objects.all()),
|
Prefetch('checkins', queryset=Checkin.objects.select_related("device")),
|
||||||
'answers', 'answers__options', 'answers__question',
|
'answers', 'answers__options', 'answers__question',
|
||||||
).select_related(
|
).select_related(
|
||||||
'item', 'order', 'order__event', 'order__event__organizer', 'seat'
|
'item', 'order', 'order__event', 'order__event__organizer', 'seat'
|
||||||
@@ -1802,17 +1826,14 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
|||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
class InvoiceFilter(FilterSet):
|
class InvoiceFilter(FilterSet):
|
||||||
refers = django_filters.CharFilter(method='refers_qs')
|
refers = django_filters.CharFilter(method='refers_qs')
|
||||||
number = django_filters.CharFilter(method='nr_qs')
|
number = MultipleCharFilter(field_name='nr', lookup_expr='iexact')
|
||||||
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
|
order = MultipleCharFilter(field_name='order', lookup_expr='code__iexact')
|
||||||
|
|
||||||
def refers_qs(self, queryset, name, value):
|
def refers_qs(self, queryset, name, value):
|
||||||
return queryset.annotate(
|
return queryset.annotate(
|
||||||
refers_nr=Concat('refers__prefix', 'refers__invoice_no')
|
refers_nr=Concat('refers__prefix', 'refers__invoice_no')
|
||||||
).filter(refers_nr__iexact=value)
|
).filter(refers_nr__iexact=value)
|
||||||
|
|
||||||
def nr_qs(self, queryset, name, value):
|
|
||||||
return queryset.filter(nr__iexact=value)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Invoice
|
model = Invoice
|
||||||
fields = ['order', 'number', 'is_cancellation', 'refers', 'locale']
|
fields = ['order', 'number', 'is_cancellation', 'refers', 'locale']
|
||||||
@@ -1898,6 +1919,7 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
return Response(status=204)
|
return Response(status=204)
|
||||||
|
|
||||||
@action(detail=True, methods=['POST'])
|
@action(detail=True, methods=['POST'])
|
||||||
|
@transaction.atomic()
|
||||||
def reissue(self, request, **kwargs):
|
def reissue(self, request, **kwargs):
|
||||||
inv = self.get_object()
|
inv = self.get_object()
|
||||||
if inv.canceled:
|
if inv.canceled:
|
||||||
@@ -1905,9 +1927,10 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
elif inv.shredded:
|
elif inv.shredded:
|
||||||
raise PermissionDenied('The invoice file is no longer stored on the server.')
|
raise PermissionDenied('The invoice file is no longer stored on the server.')
|
||||||
else:
|
else:
|
||||||
|
order = Order.objects.select_for_update(of=OF_SELF).get(pk=inv.order_id)
|
||||||
c = generate_cancellation(inv)
|
c = generate_cancellation(inv)
|
||||||
if inv.order.status != Order.STATUS_CANCELED:
|
if inv.order.status != Order.STATUS_CANCELED:
|
||||||
inv = generate_invoice(inv.order)
|
inv = generate_invoice(order)
|
||||||
else:
|
else:
|
||||||
inv = c
|
inv = c
|
||||||
inv.order.log_action(
|
inv.order.log_action(
|
||||||
|
|||||||
@@ -24,10 +24,11 @@ from decimal import Decimal
|
|||||||
import django_filters
|
import django_filters
|
||||||
from django.contrib.auth.hashers import make_password
|
from django.contrib.auth.hashers import make_password
|
||||||
from django.db import transaction
|
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.db.models.functions import Coalesce
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
from django.utils.timezone import now
|
||||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
from rest_framework import mixins, serializers, status, views, viewsets
|
from rest_framework import mixins, serializers, status, views, viewsets
|
||||||
@@ -43,18 +44,16 @@ from pretix.api.serializers.organizer import (
|
|||||||
CustomerCreateSerializer, CustomerSerializer, DeviceSerializer,
|
CustomerCreateSerializer, CustomerSerializer, DeviceSerializer,
|
||||||
GiftCardSerializer, GiftCardTransactionSerializer, MembershipSerializer,
|
GiftCardSerializer, GiftCardTransactionSerializer, MembershipSerializer,
|
||||||
MembershipTypeSerializer, OrganizerSerializer, OrganizerSettingsSerializer,
|
MembershipTypeSerializer, OrganizerSerializer, OrganizerSettingsSerializer,
|
||||||
SeatingPlanSerializer, TeamAPITokenSerializer, TeamInviteSerializer,
|
SalesChannelSerializer, SeatingPlanSerializer, TeamAPITokenSerializer,
|
||||||
TeamMemberSerializer, TeamSerializer,
|
TeamInviteSerializer, TeamMemberSerializer, TeamSerializer,
|
||||||
)
|
)
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Customer, Device, GiftCard, GiftCardTransaction, Membership,
|
Customer, Device, GiftCard, GiftCardTransaction, Membership,
|
||||||
MembershipType, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite,
|
MembershipType, Organizer, SalesChannel, SeatingPlan, Team, TeamAPIToken,
|
||||||
User,
|
TeamInvite, User,
|
||||||
)
|
)
|
||||||
from pretix.base.settings import SETTINGS_AFFECTING_CSS
|
|
||||||
from pretix.helpers import OF_SELF
|
from pretix.helpers import OF_SELF
|
||||||
from pretix.helpers.dicts import merge_dicts
|
from pretix.helpers.dicts import merge_dicts
|
||||||
from pretix.presale.style import regenerate_organizer_css
|
|
||||||
|
|
||||||
|
|
||||||
class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
|
class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
@@ -138,11 +137,19 @@ class SeatingPlanViewSet(viewsets.ModelViewSet):
|
|||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
class GiftCardFilter(FilterSet):
|
class GiftCardFilter(FilterSet):
|
||||||
secret = django_filters.CharFilter(field_name='secret', lookup_expr='iexact')
|
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:
|
class Meta:
|
||||||
model = GiftCard
|
model = GiftCard
|
||||||
fields = ['secret', 'testmode']
|
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):
|
class GiftCardViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = GiftCardSerializer
|
serializer_class = GiftCardSerializer
|
||||||
@@ -504,8 +511,6 @@ class OrganizerSettingsView(views.APIView):
|
|||||||
k: v for k, v in s.validated_data.items()
|
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={
|
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer, context={
|
||||||
'request': request
|
'request': request
|
||||||
})
|
})
|
||||||
@@ -679,3 +684,68 @@ class MembershipViewSet(viewsets.ModelViewSet):
|
|||||||
data=self.request.data,
|
data=self.request.data,
|
||||||
)
|
)
|
||||||
return inst
|
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()
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# 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/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import django_filters
|
||||||
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
|
|
||||||
from pretix.api.models import WebHook
|
from pretix.api.models import WebHook
|
||||||
@@ -26,11 +28,17 @@ from pretix.api.serializers.webhooks import WebHookSerializer
|
|||||||
from pretix.helpers.dicts import merge_dicts
|
from pretix.helpers.dicts import merge_dicts
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookFilter(FilterSet):
|
||||||
|
enabled = django_filters.rest_framework.BooleanFilter()
|
||||||
|
|
||||||
|
|
||||||
class WebHookViewSet(viewsets.ModelViewSet):
|
class WebHookViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = WebHookSerializer
|
serializer_class = WebHookSerializer
|
||||||
queryset = WebHook.objects.none()
|
queryset = WebHook.objects.none()
|
||||||
permission = 'can_change_organizer_settings'
|
permission = 'can_change_organizer_settings'
|
||||||
write_permission = 'can_change_organizer_settings'
|
write_permission = 'can_change_organizer_settings'
|
||||||
|
filter_backends = (DjangoFilterBackend,)
|
||||||
|
filterset_class = WebhookFilter
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.organizer.webhooks.prefetch_related('listeners')
|
return self.request.organizer.webhooks.prefetch_related('listeners')
|
||||||
|
|||||||
@@ -126,6 +126,17 @@ class ParametrizedOrderWebhookEvent(ParametrizedWebhookEvent):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DeletedOrderWebhookEvent(ParametrizedWebhookEvent):
|
||||||
|
def build_payload(self, logentry: LogEntry):
|
||||||
|
return {
|
||||||
|
'notification_id': logentry.pk,
|
||||||
|
'organizer': logentry.organizer.slug,
|
||||||
|
'event': logentry.event.slug,
|
||||||
|
'code': logentry.parsed_data.get("code"),
|
||||||
|
'action': logentry.action_type,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ParametrizedEventWebhookEvent(ParametrizedWebhookEvent):
|
class ParametrizedEventWebhookEvent(ParametrizedWebhookEvent):
|
||||||
|
|
||||||
def build_payload(self, logentry: LogEntry):
|
def build_payload(self, logentry: LogEntry):
|
||||||
@@ -176,7 +187,7 @@ class ParametrizedItemWebhookEvent(ParametrizedWebhookEvent):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ParametrizedOrderPositionWebhookEvent(ParametrizedOrderWebhookEvent):
|
class ParametrizedOrderPositionCheckinWebhookEvent(ParametrizedOrderWebhookEvent):
|
||||||
|
|
||||||
def build_payload(self, logentry: LogEntry):
|
def build_payload(self, logentry: LogEntry):
|
||||||
d = super().build_payload(logentry)
|
d = super().build_payload(logentry)
|
||||||
@@ -185,6 +196,7 @@ class ParametrizedOrderPositionWebhookEvent(ParametrizedOrderWebhookEvent):
|
|||||||
d['orderposition_id'] = logentry.parsed_data.get('position')
|
d['orderposition_id'] = logentry.parsed_data.get('position')
|
||||||
d['orderposition_positionid'] = logentry.parsed_data.get('positionid')
|
d['orderposition_positionid'] = logentry.parsed_data.get('positionid')
|
||||||
d['checkin_list'] = logentry.parsed_data.get('list')
|
d['checkin_list'] = logentry.parsed_data.get('list')
|
||||||
|
d['type'] = logentry.parsed_data.get('type')
|
||||||
d['first_checkin'] = logentry.parsed_data.get('first_checkin')
|
d['first_checkin'] = logentry.parsed_data.get('first_checkin')
|
||||||
return d
|
return d
|
||||||
|
|
||||||
@@ -296,11 +308,15 @@ def register_default_webhook_events(sender, **kwargs):
|
|||||||
'pretix.event.order.denied',
|
'pretix.event.order.denied',
|
||||||
_('Order denied'),
|
_('Order denied'),
|
||||||
),
|
),
|
||||||
ParametrizedOrderPositionWebhookEvent(
|
DeletedOrderWebhookEvent(
|
||||||
|
'pretix.event.order.deleted',
|
||||||
|
_('Order deleted'),
|
||||||
|
),
|
||||||
|
ParametrizedOrderPositionCheckinWebhookEvent(
|
||||||
'pretix.event.checkin',
|
'pretix.event.checkin',
|
||||||
_('Ticket checked in'),
|
_('Ticket checked in'),
|
||||||
),
|
),
|
||||||
ParametrizedOrderPositionWebhookEvent(
|
ParametrizedOrderPositionCheckinWebhookEvent(
|
||||||
'pretix.event.checkin.reverted',
|
'pretix.event.checkin.reverted',
|
||||||
_('Ticket check-in reverted'),
|
_('Ticket check-in reverted'),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class PretixBaseConfig(AppConfig):
|
|||||||
from . import invoice # NOQA
|
from . import invoice # NOQA
|
||||||
from . import notifications # NOQA
|
from . import notifications # NOQA
|
||||||
from . import email # 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 .models import _transactions # NOQA
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
|||||||
@@ -32,13 +32,16 @@
|
|||||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# 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.
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
|
import string
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import authenticate
|
from django.contrib.auth import authenticate
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.contrib.auth.hashers import check_password, make_password
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.utils.translation import gettext_lazy as _, ngettext
|
||||||
|
|
||||||
|
|
||||||
def get_auth_backends():
|
def get_auth_backends():
|
||||||
@@ -160,3 +163,62 @@ class NativeAuthBackend(BaseAuthBackend):
|
|||||||
u = authenticate(request=request, email=form_data['email'].lower(), password=form_data['password'])
|
u = authenticate(request=request, email=form_data['email'].lower(), password=form_data['password'])
|
||||||
if u and u.auth_backend == self.identifier:
|
if u and u.auth_backend == self.identifier:
|
||||||
return u
|
return u
|
||||||
|
|
||||||
|
|
||||||
|
class NumericAndAlphabeticPasswordValidator:
|
||||||
|
|
||||||
|
def validate(self, password, user=None):
|
||||||
|
has_numeric = any(c in string.digits for c in password)
|
||||||
|
has_alpha = any(c in string.ascii_letters for c in password)
|
||||||
|
if not has_numeric or not has_alpha:
|
||||||
|
raise ValidationError(
|
||||||
|
_(
|
||||||
|
"Your password must contain both numeric and alphabetic characters.",
|
||||||
|
),
|
||||||
|
code="password_numeric_and_alphabetic",
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_help_text(self):
|
||||||
|
return _(
|
||||||
|
"Your password must contain both numeric and alphabetic characters.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class HistoryPasswordValidator:
|
||||||
|
|
||||||
|
def __init__(self, history_length=4):
|
||||||
|
self.history_length = history_length
|
||||||
|
|
||||||
|
def validate(self, password, user=None):
|
||||||
|
from pretix.base.models import User
|
||||||
|
|
||||||
|
if not user or not user.pk or not isinstance(user, User):
|
||||||
|
return
|
||||||
|
|
||||||
|
for hp in user.historic_passwords.order_by("-created")[:self.history_length]:
|
||||||
|
if check_password(password, hp.password):
|
||||||
|
raise ValidationError(
|
||||||
|
ngettext(
|
||||||
|
"Your password may not be the same as your previous password.",
|
||||||
|
"Your password may not be the same as one of your %(history_length)s previous passwords.",
|
||||||
|
self.history_length,
|
||||||
|
),
|
||||||
|
code="password_history",
|
||||||
|
params={"history_length": self.history_length},
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_help_text(self):
|
||||||
|
return ngettext(
|
||||||
|
"Your password may not be the same as your previous password.",
|
||||||
|
"Your password may not be the same as one of your %(history_length)s previous passwords.",
|
||||||
|
self.history_length,
|
||||||
|
) % {"history_length": self.history_length}
|
||||||
|
|
||||||
|
def password_changed(self, password, user=None):
|
||||||
|
if not user:
|
||||||
|
pass
|
||||||
|
|
||||||
|
user.historic_passwords.create(password=make_password(password))
|
||||||
|
user.historic_passwords.filter(
|
||||||
|
pk__in=user.historic_passwords.order_by("-created")[self.history_length:].values_list("pk", flat=True),
|
||||||
|
).delete()
|
||||||
|
|||||||
@@ -20,56 +20,83 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import logging
|
import logging
|
||||||
|
import warnings
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.signals import register_sales_channels
|
from pretix.base.signals import (
|
||||||
|
register_sales_channel_types, register_sales_channels,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
_ALL_CHANNELS = None
|
_ALL_CHANNEL_TYPES = None
|
||||||
|
|
||||||
|
|
||||||
class SalesChannel:
|
class SalesChannelType:
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<SalesChannel: {}>'.format(self.identifier)
|
return '<SalesChannelType: {}>'.format(self.identifier)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def identifier(self) -> str:
|
def identifier(self) -> str:
|
||||||
"""
|
"""
|
||||||
The internal identifier of this sales channel.
|
The internal identifier of this sales channel type.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError() # NOQA
|
raise NotImplementedError() # NOQA
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def verbose_name(self) -> str:
|
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
|
raise NotImplementedError() # NOQA
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self) -> str:
|
||||||
|
"""
|
||||||
|
A human-readable description of this sales channel type.
|
||||||
|
"""
|
||||||
|
return ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def icon(self) -> str:
|
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"
|
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
|
@property
|
||||||
def testmode_supported(self) -> bool:
|
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
|
return True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def payment_restrictions_supported(self) -> bool:
|
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
|
Example: pretixPOS provides its own sales channel type, ignores the configured payment providers completely and
|
||||||
handles payments locally. Therefor, this property should be set to ``False`` for the pretixPOS sales channel as
|
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.
|
the event organizer cannot restrict the usage of any payment provider through the backend.
|
||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
@@ -77,8 +104,8 @@ class SalesChannel:
|
|||||||
@property
|
@property
|
||||||
def unlimited_items_per_order(self) -> bool:
|
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
|
If this property is ``True``, purchases made using sales channels of this type are not limited to the maximum
|
||||||
items defined in the event settings.
|
amount of items defined in the event settings.
|
||||||
"""
|
"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -96,34 +123,67 @@ class SalesChannel:
|
|||||||
"""
|
"""
|
||||||
return True
|
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:
|
def get_all_sales_channel_types():
|
||||||
return _ALL_CHANNELS
|
from pretix.base.signals import register_sales_channel_types
|
||||||
|
global _ALL_CHANNEL_TYPES
|
||||||
|
|
||||||
|
if _ALL_CHANNEL_TYPES:
|
||||||
|
return _ALL_CHANNEL_TYPES
|
||||||
|
|
||||||
channels = []
|
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)):
|
if isinstance(ret, (list, tuple)):
|
||||||
channels += ret
|
channels += ret
|
||||||
else:
|
else:
|
||||||
channels.append(ret)
|
channels.append(ret)
|
||||||
channels.sort(key=lambda c: c.identifier)
|
channels.sort(key=lambda c: c.identifier)
|
||||||
_ALL_CHANNELS = OrderedDict([(c.identifier, c) for c in channels])
|
_ALL_CHANNEL_TYPES = OrderedDict([(c.identifier, c) for c in channels])
|
||||||
if 'web' in _ALL_CHANNELS:
|
if 'web' in _ALL_CHANNEL_TYPES:
|
||||||
_ALL_CHANNELS.move_to_end('web', last=False)
|
_ALL_CHANNEL_TYPES.move_to_end('web', last=False)
|
||||||
return _ALL_CHANNELS
|
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"
|
identifier = "web"
|
||||||
verbose_name = _('Online shop')
|
verbose_name = _('Online shop')
|
||||||
icon = "globe"
|
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):
|
def base_sales_channels(sender, **kwargs):
|
||||||
return (
|
return (
|
||||||
WebshopSalesChannel(),
|
WebshopSalesChannelType(),
|
||||||
|
ApiSalesChannelType(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ This module contains utilities for implementing OpenID Connect for customer auth
|
|||||||
as well as an OpenID Provider (OP).
|
as well as an OpenID Provider (OP).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
pretix_token_endpoint_auth_methods = ['client_secret_basic', 'client_secret_post']
|
||||||
|
|
||||||
|
|
||||||
def _urljoin(base, path):
|
def _urljoin(base, path):
|
||||||
if not base.endswith("/"):
|
if not base.endswith("/"):
|
||||||
@@ -127,6 +129,16 @@ def oidc_validate_and_complete_config(config):
|
|||||||
fields=", ".join(provider_config.get("claims_supported", []))
|
fields=", ".join(provider_config.get("claims_supported", []))
|
||||||
))
|
))
|
||||||
|
|
||||||
|
if "token_endpoint_auth_methods_supported" in provider_config:
|
||||||
|
token_endpoint_auth_methods_supported = provider_config.get("token_endpoint_auth_methods_supported",
|
||||||
|
["client_secret_basic"])
|
||||||
|
if not any(x in pretix_token_endpoint_auth_methods for x in token_endpoint_auth_methods_supported):
|
||||||
|
raise ValidationError(
|
||||||
|
_(f'No supported Token Endpoint Auth Methods supported: {token_endpoint_auth_methods_supported}').format(
|
||||||
|
token_endpoint_auth_methods_supported=", ".join(token_endpoint_auth_methods_supported)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
config['provider_config'] = provider_config
|
config['provider_config'] = provider_config
|
||||||
return config
|
return config
|
||||||
|
|
||||||
@@ -147,6 +159,18 @@ def oidc_authorize_url(provider, state, redirect_uri):
|
|||||||
|
|
||||||
def oidc_validate_authorization(provider, code, redirect_uri):
|
def oidc_validate_authorization(provider, code, redirect_uri):
|
||||||
endpoint = provider.configuration['provider_config']['token_endpoint']
|
endpoint = provider.configuration['provider_config']['token_endpoint']
|
||||||
|
|
||||||
|
# Wall of shame and RFC ignorant IDPs
|
||||||
|
if endpoint == 'https://www.linkedin.com/oauth/v2/accessToken':
|
||||||
|
token_endpoint_auth_method = 'client_secret_post'
|
||||||
|
else:
|
||||||
|
token_endpoint_auth_methods = provider.configuration['provider_config'].get(
|
||||||
|
'token_endpoint_auth_methods_supported', ['client_secret_basic']
|
||||||
|
)
|
||||||
|
token_endpoint_auth_method = [
|
||||||
|
x for x in pretix_token_endpoint_auth_methods if x in token_endpoint_auth_methods
|
||||||
|
][0]
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
# https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
|
# https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
|
||||||
# https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
|
# https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
|
||||||
@@ -154,6 +178,11 @@ def oidc_validate_authorization(provider, code, redirect_uri):
|
|||||||
'code': code,
|
'code': code,
|
||||||
'redirect_uri': redirect_uri,
|
'redirect_uri': redirect_uri,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if token_endpoint_auth_method == 'client_secret_post':
|
||||||
|
params['client_id'] = provider.configuration['client_id']
|
||||||
|
params['client_secret'] = provider.configuration['client_secret']
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resp = requests.post(
|
resp = requests.post(
|
||||||
endpoint,
|
endpoint,
|
||||||
@@ -161,7 +190,10 @@ def oidc_validate_authorization(provider, code, redirect_uri):
|
|||||||
headers={
|
headers={
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
},
|
},
|
||||||
auth=(provider.configuration['client_id'], provider.configuration['client_secret']),
|
auth=(
|
||||||
|
provider.configuration['client_id'],
|
||||||
|
provider.configuration['client_secret']
|
||||||
|
) if token_endpoint_auth_method == 'client_secret_basic' else None,
|
||||||
)
|
)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
|
|||||||
@@ -207,10 +207,13 @@ class ListExporter(BaseExporter):
|
|||||||
def get_filename(self):
|
def get_filename(self):
|
||||||
return 'export'
|
return 'export'
|
||||||
|
|
||||||
|
def get_csv_encoding(self):
|
||||||
|
return 'utf-8'
|
||||||
|
|
||||||
def _render_csv(self, form_data, output_file=None, **kwargs):
|
def _render_csv(self, form_data, output_file=None, **kwargs):
|
||||||
if output_file:
|
if output_file:
|
||||||
if 'b' in output_file.mode:
|
if 'b' in output_file.mode:
|
||||||
output_file = io.TextIOWrapper(output_file, encoding='utf-8', newline='')
|
output_file = io.TextIOWrapper(output_file, encoding=self.get_csv_encoding(), errors='replace', newline='')
|
||||||
writer = csv.writer(output_file, **kwargs)
|
writer = csv.writer(output_file, **kwargs)
|
||||||
total = 0
|
total = 0
|
||||||
counter = 0
|
counter = 0
|
||||||
@@ -246,7 +249,7 @@ class ListExporter(BaseExporter):
|
|||||||
if counter % max(10, total // 100) == 0:
|
if counter % max(10, total // 100) == 0:
|
||||||
self.progress_callback(counter / total * 100)
|
self.progress_callback(counter / total * 100)
|
||||||
writer.writerow(line)
|
writer.writerow(line)
|
||||||
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
|
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode(self.get_csv_encoding(), errors='replace')
|
||||||
|
|
||||||
def prepare_xlsx_sheet(self, ws):
|
def prepare_xlsx_sheet(self, ws):
|
||||||
pass
|
pass
|
||||||
@@ -256,7 +259,7 @@ class ListExporter(BaseExporter):
|
|||||||
ws = wb.create_sheet()
|
ws = wb.create_sheet()
|
||||||
self.prepare_xlsx_sheet(ws)
|
self.prepare_xlsx_sheet(ws)
|
||||||
try:
|
try:
|
||||||
ws.title = str(self.verbose_name)
|
ws.title = str(self.verbose_name)[:30]
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
total = 0
|
total = 0
|
||||||
@@ -374,7 +377,7 @@ class MultiSheetListExporter(ListExporter):
|
|||||||
wb = SafeWorkbook(write_only=True)
|
wb = SafeWorkbook(write_only=True)
|
||||||
n_sheets = len(self.sheets)
|
n_sheets = len(self.sheets)
|
||||||
for i_sheet, (s, l) in enumerate(self.sheets):
|
for i_sheet, (s, l) in enumerate(self.sheets):
|
||||||
ws = wb.create_sheet(str(l))
|
ws = wb.create_sheet(str(l)[:30])
|
||||||
if hasattr(self, 'prepare_xlsx_sheet_' + s):
|
if hasattr(self, 'prepare_xlsx_sheet_' + s):
|
||||||
getattr(self, 'prepare_xlsx_sheet_' + s)(ws)
|
getattr(self, 'prepare_xlsx_sheet_' + s)(ws)
|
||||||
|
|
||||||
|
|||||||
@@ -39,10 +39,12 @@ from zipfile import ZipFile
|
|||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
|
|
||||||
from pretix.base.models import QuestionAnswer
|
from pretix.base.models import QuestionAnswer
|
||||||
|
|
||||||
|
from ...control.forms.widgets import Select2
|
||||||
from ..exporter import BaseExporter
|
from ..exporter import BaseExporter
|
||||||
from ..signals import register_data_exporters
|
from ..signals import register_data_exporters
|
||||||
|
|
||||||
@@ -56,7 +58,7 @@ class AnswerFilesExporter(BaseExporter):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def export_form_fields(self):
|
def export_form_fields(self):
|
||||||
return OrderedDict(
|
d = OrderedDict(
|
||||||
[
|
[
|
||||||
('questions',
|
('questions',
|
||||||
forms.ModelMultipleChoiceField(
|
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):
|
def render(self, form_data: dict):
|
||||||
qs = QuestionAnswer.objects.filter(
|
qs = QuestionAnswer.objects.filter(
|
||||||
orderposition__order__event=self.event,
|
orderposition__order__event=self.event,
|
||||||
).select_related('orderposition', 'orderposition__order', 'question')
|
).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'):
|
if form_data.get('questions'):
|
||||||
qs = qs.filter(question__in=form_data['questions'])
|
qs = qs.filter(question__in=form_data['questions'])
|
||||||
with tempfile.TemporaryDirectory() as d:
|
with tempfile.TemporaryDirectory() as d:
|
||||||
|
|||||||
@@ -116,15 +116,29 @@ class DekodiNREIExporter(BaseExporter):
|
|||||||
'PTNo15': p.full_id or '',
|
'PTNo15': p.full_id or '',
|
||||||
})
|
})
|
||||||
elif p.provider and p.provider.startswith('stripe'):
|
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({
|
payments.append({
|
||||||
'PTID': '81',
|
'PTID': '81',
|
||||||
'PTN': 'Stripe',
|
'PTN': 'Stripe',
|
||||||
'PTNo1': p.info_data.get("id") or '',
|
'PTNo1': pi.get("id") or '',
|
||||||
'PTNo5': src.get("card", {}).get("last4") or '',
|
'PTNo5': card.get("last4", ""),
|
||||||
'PTNo7': round(float(p.amount), 2) or '',
|
'PTNo7': round(float(p.amount), 2) or '',
|
||||||
'PTNo8': str(self.event.currency) 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 '',
|
'PTNo15': p.full_id or '',
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ from openpyxl.styles import Alignment
|
|||||||
from openpyxl.utils import get_column_letter
|
from openpyxl.utils import get_column_letter
|
||||||
|
|
||||||
from ...helpers.safe_openpyxl import SafeCell
|
from ...helpers.safe_openpyxl import SafeCell
|
||||||
from ..channels import get_all_sales_channels
|
|
||||||
from ..exporter import ListExporter
|
from ..exporter import ListExporter
|
||||||
from ..models import ItemMetaValue, ItemVariation, ItemVariationMetaValue
|
from ..models import ItemMetaValue, ItemVariation, ItemVariationMetaValue
|
||||||
from ..signals import register_data_exporters
|
from ..signals import register_data_exporters
|
||||||
@@ -53,7 +52,7 @@ class ItemDataExporter(ListExporter):
|
|||||||
|
|
||||||
def iterate_list(self, form_data):
|
def iterate_list(self, form_data):
|
||||||
locales = self.event.settings.locales
|
locales = self.event.settings.locales
|
||||||
scs = get_all_sales_channels()
|
scs = self.organizer.sales_channels.all()
|
||||||
header = [
|
header = [
|
||||||
_("Product ID"),
|
_("Product ID"),
|
||||||
_("Variation ID"),
|
_("Variation ID"),
|
||||||
@@ -141,9 +140,15 @@ class ItemDataExporter(ListExporter):
|
|||||||
row.append(i.name.localize(l))
|
row.append(i.name.localize(l))
|
||||||
for l in locales:
|
for l in locales:
|
||||||
row.append(v.value.localize(l))
|
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 += [
|
row += [
|
||||||
_("Yes") if i.active and v.active else "",
|
_("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,
|
v.default_price or i.default_price,
|
||||||
_("Yes") if i.free_price else "",
|
_("Yes") if i.free_price else "",
|
||||||
str(i.tax_rule) if i.tax_rule else "",
|
str(i.tax_rule) if i.tax_rule else "",
|
||||||
@@ -186,9 +191,12 @@ class ItemDataExporter(ListExporter):
|
|||||||
row.append(i.name.localize(l))
|
row.append(i.name.localize(l))
|
||||||
for l in locales:
|
for l in locales:
|
||||||
row.append("")
|
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 += [
|
row += [
|
||||||
_("Yes") if i.active else "",
|
_("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,
|
i.default_price,
|
||||||
_("Yes") if i.free_price else "",
|
_("Yes") if i.free_price else "",
|
||||||
str(i.tax_rule) if i.tax_rule else "",
|
str(i.tax_rule) if i.tax_rule else "",
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ class JSONExporter(BaseExporter):
|
|||||||
'import in third-party systems.')
|
'import in third-party systems.')
|
||||||
|
|
||||||
def render(self, form_data):
|
def render(self, form_data):
|
||||||
|
all_sales_channels = self.event.organizer.sales_channels.all()
|
||||||
jo = {
|
jo = {
|
||||||
'event': {
|
'event': {
|
||||||
'name': str(self.event.name),
|
'name': str(self.event.name),
|
||||||
@@ -85,7 +86,7 @@ class JSONExporter(BaseExporter):
|
|||||||
'admission': item.admission,
|
'admission': item.admission,
|
||||||
'personalized': item.personalized,
|
'personalized': item.personalized,
|
||||||
'active': item.active,
|
'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),
|
'description': str(item.description),
|
||||||
'available_from': item.available_from,
|
'available_from': item.available_from,
|
||||||
'available_until': item.available_until,
|
'available_until': item.available_until,
|
||||||
@@ -114,7 +115,9 @@ class JSONExporter(BaseExporter):
|
|||||||
'checkin_text': variation.checkin_text,
|
'checkin_text': variation.checkin_text,
|
||||||
'require_approval': variation.require_approval,
|
'require_approval': variation.require_approval,
|
||||||
'require_membership': variation.require_membership,
|
'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_from': variation.available_from,
|
||||||
'available_until': variation.available_until,
|
'available_until': variation.available_until,
|
||||||
'hide_without_voucher': variation.hide_without_voucher,
|
'hide_without_voucher': variation.hide_without_voucher,
|
||||||
@@ -122,6 +125,7 @@ class JSONExporter(BaseExporter):
|
|||||||
} for variation in item.variations.all()
|
} for variation in item.variations.all()
|
||||||
]
|
]
|
||||||
} for item in self.event.items.select_related('tax_rule').prefetch_related(
|
} for item in self.event.items.select_related('tax_rule').prefetch_related(
|
||||||
|
'limit_sales_channels',
|
||||||
Prefetch(
|
Prefetch(
|
||||||
'meta_values',
|
'meta_values',
|
||||||
ItemMetaValue.objects.select_related('property'),
|
ItemMetaValue.objects.select_related('property'),
|
||||||
@@ -130,6 +134,7 @@ class JSONExporter(BaseExporter):
|
|||||||
Prefetch(
|
Prefetch(
|
||||||
'variations',
|
'variations',
|
||||||
queryset=ItemVariation.objects.prefetch_related(
|
queryset=ItemVariation.objects.prefetch_related(
|
||||||
|
'limit_sales_channels',
|
||||||
Prefetch(
|
Prefetch(
|
||||||
'meta_values',
|
'meta_values',
|
||||||
ItemVariationMetaValue.objects.select_related('property'),
|
ItemVariationMetaValue.objects.select_related('property'),
|
||||||
@@ -167,7 +172,7 @@ class JSONExporter(BaseExporter):
|
|||||||
'require_approval': order.require_approval,
|
'require_approval': order.require_approval,
|
||||||
'checkin_attention': order.checkin_attention,
|
'checkin_attention': order.checkin_attention,
|
||||||
'checkin_text': order.checkin_text,
|
'checkin_text': order.checkin_text,
|
||||||
'sales_channel': order.sales_channel,
|
'sales_channel': order.sales_channel.identifier,
|
||||||
'expires': order.expires,
|
'expires': order.expires,
|
||||||
'datetime': order.datetime,
|
'datetime': order.datetime,
|
||||||
'fees': [
|
'fees': [
|
||||||
|
|||||||
@@ -32,11 +32,12 @@
|
|||||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# 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.
|
# 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 decimal import Decimal
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.conf import settings
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Case, CharField, Count, DateTimeField, F, IntegerField, Max, Min, OuterRef,
|
Case, CharField, Count, DateTimeField, F, IntegerField, Max, Min, OuterRef,
|
||||||
Q, Subquery, Sum, When,
|
Q, Subquery, Sum, When,
|
||||||
@@ -54,7 +55,7 @@ from openpyxl.comments import Comment
|
|||||||
from openpyxl.styles import Font, PatternFill
|
from openpyxl.styles import Font, PatternFill
|
||||||
|
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
GiftCard, GiftCardTransaction, Invoice, InvoiceAddress, Order,
|
Checkin, GiftCard, GiftCardTransaction, Invoice, InvoiceAddress, Order,
|
||||||
OrderPosition, Question,
|
OrderPosition, Question,
|
||||||
)
|
)
|
||||||
from pretix.base.models.orders import (
|
from pretix.base.models.orders import (
|
||||||
@@ -541,9 +542,25 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
).order_by()
|
).order_by()
|
||||||
qs = base_qs.annotate(
|
qs = base_qs.annotate(
|
||||||
payment_providers=Subquery(p_providers, output_field=CharField()),
|
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(
|
).select_related(
|
||||||
'order', 'order__invoice_address', 'order__customer', 'item', 'variation',
|
'order', 'order__invoice_address', 'order__customer', 'item', 'variation',
|
||||||
'voucher', 'tax_rule'
|
'voucher', 'tax_rule', 'addon_to',
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'subevent', 'subevent__meta_values',
|
'subevent', 'subevent__meta_values',
|
||||||
'answers', 'answers__question', 'answers__options'
|
'answers', 'answers__question', 'answers__options'
|
||||||
@@ -602,13 +619,13 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
_('Valid until'),
|
_('Valid until'),
|
||||||
_('Order comment'),
|
_('Order comment'),
|
||||||
_('Follow-up date'),
|
_('Follow-up date'),
|
||||||
|
_('Add-on to position ID'),
|
||||||
]
|
]
|
||||||
|
|
||||||
questions = list(Question.objects.filter(event__in=self.events))
|
questions = list(Question.objects.filter(event__in=self.events))
|
||||||
options = {}
|
options = defaultdict(list)
|
||||||
for q in questions:
|
for q in questions:
|
||||||
if q.type == Question.TYPE_CHOICE_MULTIPLE:
|
if q.type == Question.TYPE_CHOICE_MULTIPLE:
|
||||||
options[q.pk] = []
|
|
||||||
if form_data['group_multiple_choice']:
|
if form_data['group_multiple_choice']:
|
||||||
for o in q.options.all():
|
for o in q.options.all():
|
||||||
options[q.pk].append(o)
|
options[q.pk].append(o)
|
||||||
@@ -618,6 +635,9 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
headers.append(str(q.question) + ' – ' + str(o.answer))
|
headers.append(str(q.question) + ' – ' + str(o.answer))
|
||||||
options[q.pk].append(o)
|
options[q.pk].append(o)
|
||||||
else:
|
else:
|
||||||
|
if q.type == Question.TYPE_CHOICE:
|
||||||
|
for o in q.options.all():
|
||||||
|
options[q.pk].append(o)
|
||||||
headers.append(str(q.question))
|
headers.append(str(q.question))
|
||||||
headers += [
|
headers += [
|
||||||
_('Company'),
|
_('Company'),
|
||||||
@@ -633,9 +653,11 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
_('VAT ID'),
|
_('VAT ID'),
|
||||||
]
|
]
|
||||||
headers += [
|
headers += [
|
||||||
_('Sales channel'), _('Order locale'),
|
_('Sales channel'),
|
||||||
|
_('Order locale'),
|
||||||
_('E-mail address verified'),
|
_('E-mail address verified'),
|
||||||
_('External customer ID'),
|
_('External customer ID'),
|
||||||
|
_('Check-in lists'),
|
||||||
_('Payment providers'),
|
_('Payment providers'),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -723,11 +745,12 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
]
|
]
|
||||||
row.append(order.comment)
|
row.append(order.comment)
|
||||||
row.append(order.custom_followup_at.strftime("%Y-%m-%d") if order.custom_followup_at else "")
|
row.append(order.custom_followup_at.strftime("%Y-%m-%d") if order.custom_followup_at else "")
|
||||||
|
row.append(op.addon_to.positionid if op.addon_to_id else "")
|
||||||
acache = {}
|
acache = {}
|
||||||
for a in op.answers.all():
|
for a in op.answers.all():
|
||||||
# We do not want to localize Date, Time and Datetime question answers, as those can lead
|
# We do not want to localize Date, Time and Datetime question answers, as those can lead
|
||||||
# to difficulties parsing the data (for example 2019-02-01 may become Février, 2019 01 in French).
|
# to difficulties parsing the data (for example 2019-02-01 may become Février, 2019 01 in French).
|
||||||
if a.question.type == 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())
|
acache[a.question_id] = set(o.pk for o in a.options.all())
|
||||||
elif a.question.type in Question.UNLOCALIZED_TYPES:
|
elif a.question.type in Question.UNLOCALIZED_TYPES:
|
||||||
acache[a.question_id] = a.answer
|
acache[a.question_id] = a.answer
|
||||||
@@ -740,6 +763,10 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
else:
|
else:
|
||||||
for o in options[q.pk]:
|
for o in options[q.pk]:
|
||||||
row.append(_('Yes') if o.pk in acache.get(q.pk, set()) else _('No'))
|
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:
|
else:
|
||||||
row.append(acache.get(q.pk, ''))
|
row.append(acache.get(q.pk, ''))
|
||||||
|
|
||||||
@@ -770,6 +797,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
_('Yes') if order.email_known_to_work else _('No'),
|
_('Yes') if order.email_known_to_work else _('No'),
|
||||||
str(order.customer.external_identifier) if order.customer and order.customer.external_identifier else '',
|
str(order.customer.external_identifier) if order.customer and order.customer.external_identifier else '',
|
||||||
]
|
]
|
||||||
|
row.append(op.checked_in_lists or "")
|
||||||
row.append(', '.join([
|
row.append(', '.join([
|
||||||
str(self.providers.get(p, p)) for p in sorted(set((op.payment_providers or '').split(',')))
|
str(self.providers.get(p, p)) for p in sorted(set((op.payment_providers or '').split(',')))
|
||||||
if p and p != 'free'
|
if p and p != 'free'
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ from django import forms
|
|||||||
from django.core.validators import URLValidator
|
from django.core.validators import URLValidator
|
||||||
from django.forms.models import ModelFormMetaclass
|
from django.forms.models import ModelFormMetaclass
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from formtools.wizard.views import SessionWizardView
|
from formtools.wizard.views import SessionWizardView
|
||||||
from hierarkey.forms import HierarkeyForm
|
from hierarkey.forms import HierarkeyForm
|
||||||
@@ -85,6 +86,43 @@ class I18nInlineFormSet(i18nfield.forms.I18nInlineFormSet):
|
|||||||
super().__init__(*args, **kwargs)
|
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 = '*****'
|
SECRET_REDACTED = '*****'
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import ipaddress
|
import ipaddress
|
||||||
|
import logging
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
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.functional import cached_property
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from pretix.base.metrics import pretix_failed_logins
|
||||||
from pretix.base.models import User
|
from pretix.base.models import User
|
||||||
from pretix.helpers.dicts import move_to_end
|
from pretix.helpers.dicts import move_to_end
|
||||||
from pretix.helpers.http import get_client_ip
|
from pretix.helpers.http import get_client_ip
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class LoginForm(forms.Form):
|
class LoginForm(forms.Form):
|
||||||
"""
|
"""
|
||||||
@@ -55,6 +59,7 @@ class LoginForm(forms.Form):
|
|||||||
username/password logins.
|
username/password logins.
|
||||||
"""
|
"""
|
||||||
keep_logged_in = forms.BooleanField(label=_("Keep me logged in"), required=False)
|
keep_logged_in = forms.BooleanField(label=_("Keep me logged in"), required=False)
|
||||||
|
origin = forms.CharField(widget=forms.HiddenInput, required=False)
|
||||||
|
|
||||||
error_messages = {
|
error_messages = {
|
||||||
'invalid_login': _("This combination of credentials is not known to our system."),
|
'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")
|
rc = get_redis_connection("redis")
|
||||||
cnt = rc.get(self.ratelimit_key)
|
cnt = rc.get(self.ratelimit_key)
|
||||||
if cnt and int(cnt) > 10:
|
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')
|
raise forms.ValidationError(self.error_messages['rate_limit'], code='rate_limit')
|
||||||
self.user_cache = self.backend.form_authenticate(self.request, self.cleaned_data)
|
self.user_cache = self.backend.form_authenticate(self.request, self.cleaned_data)
|
||||||
if self.user_cache is None:
|
if self.user_cache is None:
|
||||||
if self.ratelimit_key:
|
if self.ratelimit_key:
|
||||||
rc.incr(self.ratelimit_key)
|
rc.incr(self.ratelimit_key)
|
||||||
rc.expire(self.ratelimit_key, 300)
|
rc.expire(self.ratelimit_key, 300)
|
||||||
|
logger.info("Backend login invalid.")
|
||||||
|
pretix_failed_logins.inc(1, reason="invalid")
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
self.error_messages['invalid_login'],
|
self.error_messages['invalid_login'],
|
||||||
code='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 the given user may log in, this method should return None.
|
||||||
"""
|
"""
|
||||||
if not user.is_active:
|
if not user.is_active:
|
||||||
|
logger.info("Backend login rejected due to user inactive.")
|
||||||
|
pretix_failed_logins.inc(1, reason="inactive")
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
self.error_messages['inactive'],
|
self.error_messages['inactive'],
|
||||||
code='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.formats import date_format
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.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.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
from django_countries import countries
|
from django_countries import countries
|
||||||
from django_countries.fields import Country, CountryField
|
from django_countries.fields import Country, CountryField
|
||||||
@@ -86,6 +86,7 @@ from pretix.base.settings import (
|
|||||||
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS,
|
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS,
|
||||||
)
|
)
|
||||||
from pretix.base.templatetags.rich_text import rich_text
|
from pretix.base.templatetags.rich_text import rich_text
|
||||||
|
from pretix.base.timemachine import time_machine_now
|
||||||
from pretix.control.forms import (
|
from pretix.control.forms import (
|
||||||
ExtFileField, ExtValidationMixin, SizeValidationMixin, SplitDateTimeField,
|
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 cartpos and item.validity_mode == Item.VALIDITY_MODE_DYNAMIC and item.validity_dynamic_start_choice:
|
||||||
if item.validity_dynamic_start_choice_day_limit:
|
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:
|
else:
|
||||||
max_date = None
|
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:
|
if item.validity_dynamic_duration_months or item.validity_dynamic_duration_days:
|
||||||
attrs = {}
|
attrs = {}
|
||||||
if max_date:
|
if max_date:
|
||||||
attrs['data-max'] = max_date.date().isoformat()
|
attrs['data-max'] = max_date.date().isoformat()
|
||||||
|
if min_date:
|
||||||
|
attrs['data-min'] = min_date.date().isoformat()
|
||||||
self.fields['requested_valid_from'] = forms.DateField(
|
self.fields['requested_valid_from'] = forms.DateField(
|
||||||
label=_('Start date'),
|
label=_('Start date'),
|
||||||
help_text=_('If you keep this empty, the ticket will be valid starting at the time of purchase.'),
|
help_text='' if initial else _('If you keep this empty, the ticket will be valid starting at the time of purchase.'),
|
||||||
required=False,
|
required=bool(initial),
|
||||||
|
initial=pos.requested_valid_from or initial,
|
||||||
widget=DatePickerWidget(attrs),
|
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:
|
else:
|
||||||
self.fields['requested_valid_from'] = forms.SplitDateTimeField(
|
self.fields['requested_valid_from'] = forms.SplitDateTimeField(
|
||||||
label=_('Start date'),
|
label=_('Start date'),
|
||||||
help_text=_('If you keep this empty, the ticket will be valid starting at the time of purchase.'),
|
help_text='' if initial else _('If you keep this empty, the ticket will be valid starting at the time of purchase.'),
|
||||||
required=False,
|
required=bool(initial),
|
||||||
|
initial=pos.requested_valid_from or initial,
|
||||||
widget=SplitDateTimePickerWidget(
|
widget=SplitDateTimePickerWidget(
|
||||||
time_format=get_format_without_seconds('TIME_INPUT_FORMATS'),
|
time_format=get_format_without_seconds('TIME_INPUT_FORMATS'),
|
||||||
|
min_date=min_date,
|
||||||
max_date=max_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 = {}
|
add_fields = {}
|
||||||
@@ -1023,7 +1035,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
self.all_optional = kwargs.pop('all_optional', False)
|
self.all_optional = kwargs.pop('all_optional', False)
|
||||||
|
|
||||||
kwargs.setdefault('initial', {})
|
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)
|
kwargs['initial']['country'] = guess_country_from_request(self.request, self.event)
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -1110,6 +1122,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
|
|
||||||
if event.settings.invoice_address_custom_field:
|
if event.settings.invoice_address_custom_field:
|
||||||
self.fields['custom_field'].label = event.settings.invoice_address_custom_field
|
self.fields['custom_field'].label = event.settings.invoice_address_custom_field
|
||||||
|
self.fields['custom_field'].help_text = event.settings.invoice_address_custom_field_helptext
|
||||||
else:
|
else:
|
||||||
del self.fields['custom_field']
|
del self.fields['custom_field']
|
||||||
|
|
||||||
@@ -1159,7 +1172,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
self.instance.vat_id_validated = False
|
self.instance.vat_id_validated = False
|
||||||
messages.warning(self.request, e.message)
|
messages.warning(self.request, e.message)
|
||||||
else:
|
else:
|
||||||
raise ValidationError(e.message)
|
raise ValidationError({"vat_id": e.message})
|
||||||
except VATIDTemporaryError as e:
|
except VATIDTemporaryError as e:
|
||||||
self.instance.vat_id_validated = False
|
self.instance.vat_id_validated = False
|
||||||
if self.request and self.vat_warning:
|
if self.request and self.vat_warning:
|
||||||
|
|||||||
@@ -32,13 +32,13 @@
|
|||||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# 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.
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import BaseValidator
|
from django.core.validators import BaseValidator
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from i18nfield.strings import LazyI18nString
|
from i18nfield.strings import LazyI18nString
|
||||||
|
|
||||||
|
from pretix.helpers.format import format_map
|
||||||
|
|
||||||
|
|
||||||
class PlaceholderValidator(BaseValidator):
|
class PlaceholderValidator(BaseValidator):
|
||||||
"""
|
"""
|
||||||
@@ -47,6 +47,12 @@ class PlaceholderValidator(BaseValidator):
|
|||||||
which are not presented in taken list.
|
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):
|
def __init__(self, limit_value):
|
||||||
super().__init__(limit_value)
|
super().__init__(limit_value)
|
||||||
self.limit_value = limit_value
|
self.limit_value = limit_value
|
||||||
@@ -57,22 +63,15 @@ class PlaceholderValidator(BaseValidator):
|
|||||||
self.__call__(v)
|
self.__call__(v)
|
||||||
return
|
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(
|
raise ValidationError(
|
||||||
_('Invalid placeholder syntax: You used a different number of "{" than of "}".'),
|
_('Invalid placeholder: {%(value)s}'),
|
||||||
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'),
|
|
||||||
code='invalid_placeholders',
|
code='invalid_placeholders',
|
||||||
params={'value': ", ".join(invalid_placeholders,)})
|
params={'value': e.args[0]})
|
||||||
|
|
||||||
def clean(self, x):
|
def clean(self, x):
|
||||||
return x
|
return x
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user