forked from CGM_Public/pretix_original
Compare commits
907 Commits
v4.0.0
...
fix-2556-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d97648802e | ||
|
|
645a7489df | ||
|
|
dff4bf2841 | ||
|
|
fc1e2ca3b0 | ||
|
|
2a0748a008 | ||
|
|
41d3f39fc7 | ||
|
|
8e481905e2 | ||
|
|
552e130fed | ||
|
|
f8b879c2e6 | ||
|
|
9e5403333c | ||
|
|
2486c3d205 | ||
|
|
ee9bf25ae1 | ||
|
|
c4d3c48837 | ||
|
|
d97b7b4fb6 | ||
|
|
fd83e91174 | ||
|
|
6e3b62696e | ||
|
|
a0b8fedaa7 | ||
|
|
a4961d9dfb | ||
|
|
889f972768 | ||
|
|
478ab633e0 | ||
|
|
0e4047018e | ||
|
|
cbeaffa415 | ||
|
|
09972b62c9 | ||
|
|
3b2eec659d | ||
|
|
4e6c61316c | ||
|
|
c308939f3c | ||
|
|
41604ebdd9 | ||
|
|
e2adf1fdb3 | ||
|
|
90d9ddebb5 | ||
|
|
684b8e4102 | ||
|
|
a83e963ea5 | ||
|
|
c9392236f5 | ||
|
|
57d68eaddb | ||
|
|
ab1e063a93 | ||
|
|
4b76dcedc4 | ||
|
|
55752a4319 | ||
|
|
5bde98e349 | ||
|
|
1b2ca87f2d | ||
|
|
8562c2f103 | ||
|
|
ea0283266f | ||
|
|
cc912b9ea7 | ||
|
|
ebae275a2d | ||
|
|
02f8bcbe23 | ||
|
|
4ae7cc9f50 | ||
|
|
c2f2e157d7 | ||
|
|
ebf0320c2c | ||
|
|
b05fa89010 | ||
|
|
ab0f76c7bb | ||
|
|
e4c1f30b9d | ||
|
|
9ffeafa6a5 | ||
|
|
cb7e014966 | ||
|
|
7b7a8e655e | ||
|
|
ac46172f6b | ||
|
|
31eda01464 | ||
|
|
0adddb3084 | ||
|
|
0b91f9da31 | ||
|
|
0a7ba606c6 | ||
|
|
959b076b52 | ||
|
|
06b0ef906a | ||
|
|
4f1607040e | ||
|
|
38a3fa23ed | ||
|
|
baae16d9e7 | ||
|
|
e2d9cc10e8 | ||
|
|
f0f8bbb2b0 | ||
|
|
8d57544ba3 | ||
|
|
065533ee4d | ||
|
|
fb2ad2711f | ||
|
|
52004e5c46 | ||
|
|
9fcd099531 | ||
|
|
d8cf3552ba | ||
|
|
cc04f66a48 | ||
|
|
86d28b3f21 | ||
|
|
07b401b7af | ||
|
|
8ff4f778bf | ||
|
|
5174d38017 | ||
|
|
5681ea121d | ||
|
|
4e2a3b45da | ||
|
|
d796c399a6 | ||
|
|
bee5b5f1af | ||
|
|
028be3a5c3 | ||
|
|
990fd9a40c | ||
|
|
746c9af531 | ||
|
|
47208afcec | ||
|
|
b215a0bd04 | ||
|
|
6c94b62bb1 | ||
|
|
d7a32d985b | ||
|
|
fac3e7f7bd | ||
|
|
8e8dce7ccf | ||
|
|
465e11c765 | ||
|
|
7e6c512a76 | ||
|
|
5d0b8c5084 | ||
|
|
6008e3d11c | ||
|
|
aeeebc0b4b | ||
|
|
a568a37f4c | ||
|
|
b74f5508b7 | ||
|
|
a9b0651345 | ||
|
|
c126445fe0 | ||
|
|
f8b8d92f2c | ||
|
|
a66fdc5084 | ||
|
|
6d6883b343 | ||
|
|
482968175b | ||
|
|
5d6302d5fd | ||
|
|
92fbe76327 | ||
|
|
2fd2462ef1 | ||
|
|
19881dff44 | ||
|
|
798fdbf25b | ||
|
|
1718a537e6 | ||
|
|
b4d8936b78 | ||
|
|
683bc3f6dc | ||
|
|
b79c95f334 | ||
|
|
7821ba09ec | ||
|
|
af2600fd52 | ||
|
|
058282a583 | ||
|
|
16fa01ac60 | ||
|
|
2b5ce5364b | ||
|
|
4a93866cc3 | ||
|
|
fbc1d862a1 | ||
|
|
638daa2c19 | ||
|
|
d400a3c7d3 | ||
|
|
76f6947529 | ||
|
|
db7e299af1 | ||
|
|
7ed204ffc0 | ||
|
|
14e0d9cbf4 | ||
|
|
65fb492728 | ||
|
|
a4f64e94cc | ||
|
|
67ba1f81e4 | ||
|
|
cc8282bef1 | ||
|
|
c7fc52cabe | ||
|
|
4bc04de325 | ||
|
|
9a2ecae021 | ||
|
|
e55fb303c0 | ||
|
|
185761e9e6 | ||
|
|
a78cb039da | ||
|
|
5f07f0e80b | ||
|
|
4021b28d5f | ||
|
|
8717b1f8db | ||
|
|
b789e64830 | ||
|
|
4d595e3fd4 | ||
|
|
482a9c6af7 | ||
|
|
99faa8b300 | ||
|
|
f0be03f93a | ||
|
|
5f12fca88a | ||
|
|
fa88686856 | ||
|
|
59a6e4130e | ||
|
|
35ccc3a9af | ||
|
|
ef9e7fd92a | ||
|
|
d7acd2b6bf | ||
|
|
2bf5a0ce8a | ||
|
|
7310fb3c6e | ||
|
|
069dd02ebc | ||
|
|
70e4b02370 | ||
|
|
b20797fe4b | ||
|
|
aee8de54ed | ||
|
|
6d7e16c147 | ||
|
|
f511f5a646 | ||
|
|
6ba690932f | ||
|
|
46b3e3c739 | ||
|
|
3550197fc4 | ||
|
|
db96211c7a | ||
|
|
758179f12f | ||
|
|
98409b0a22 | ||
|
|
06ffa0bcd5 | ||
|
|
18917769ef | ||
|
|
34e95bc7d2 | ||
|
|
5ba7ee3516 | ||
|
|
3706eff795 | ||
|
|
28331e7538 | ||
|
|
62218ca0c6 | ||
|
|
14e2834a72 | ||
|
|
032653cec4 | ||
|
|
f3b355e9f3 | ||
|
|
f7d2645e76 | ||
|
|
fb89e31c1c | ||
|
|
5e1cff53b4 | ||
|
|
61cef87c9d | ||
|
|
2fcab70e3b | ||
|
|
1414db35b7 | ||
|
|
1d32d7a2d2 | ||
|
|
9966912799 | ||
|
|
a37ed6f001 | ||
|
|
a307cf8934 | ||
|
|
0e900b74d7 | ||
|
|
7193da42c2 | ||
|
|
48eb580ee8 | ||
|
|
50a5622178 | ||
|
|
66027aed59 | ||
|
|
8d62e3e2af | ||
|
|
a8ce4845e2 | ||
|
|
efdb834a73 | ||
|
|
fd060b792c | ||
|
|
31df3e2129 | ||
|
|
c71ba79e55 | ||
|
|
6304b34600 | ||
|
|
81cc7540ec | ||
|
|
adced71706 | ||
|
|
8c7ed38441 | ||
|
|
b4d7d9bf76 | ||
|
|
af3006a5bd | ||
|
|
d313e076a2 | ||
|
|
216bac2807 | ||
|
|
8351e51cfe | ||
|
|
b2d74dc652 | ||
|
|
ea1322165b | ||
|
|
c65883b328 | ||
|
|
dfd37cc5e3 | ||
|
|
4c71995560 | ||
|
|
02034cacbf | ||
|
|
d098cda8a8 | ||
|
|
0b8432b2c5 | ||
|
|
9be6ad4124 | ||
|
|
fdc77f6bd8 | ||
|
|
e3d0a18bee | ||
|
|
81c271ee2a | ||
|
|
e981f00dc7 | ||
|
|
2daf35c39e | ||
|
|
c9530c56af | ||
|
|
f3e31287f4 | ||
|
|
c9aaa343e6 | ||
|
|
87a196c4df | ||
|
|
a220f1678b | ||
|
|
c8fa0852b2 | ||
|
|
fe3433106c | ||
|
|
f8086daf34 | ||
|
|
66f75a5614 | ||
|
|
6f30c347c0 | ||
|
|
3596fa9c5a | ||
|
|
e3c7cd7c6d | ||
|
|
194042dca5 | ||
|
|
3be6e83f33 | ||
|
|
4262bce2b5 | ||
|
|
73ab962e16 | ||
|
|
13a86fc6f3 | ||
|
|
9d6f11718a | ||
|
|
c9d3428996 | ||
|
|
d4ef16b31a | ||
|
|
6a35e7d3cd | ||
|
|
463443d606 | ||
|
|
6f0da5c2ca | ||
|
|
c1344422a5 | ||
|
|
c2bd3dde44 | ||
|
|
9e51736232 | ||
|
|
5b27ce1265 | ||
|
|
0757542f4f | ||
|
|
12be98c888 | ||
|
|
51e6b02aa9 | ||
|
|
acc4a167b1 | ||
|
|
dd9429bbfa | ||
|
|
768bb8c106 | ||
|
|
cbdafac999 | ||
|
|
96f694cf61 | ||
|
|
5576829ebf | ||
|
|
b0d67e92ac | ||
|
|
63e28723d2 | ||
|
|
cc0656f169 | ||
|
|
849c8e719a | ||
|
|
a3ec2a4061 | ||
|
|
00a7187a7a | ||
|
|
701c4f768e | ||
|
|
cf751d38d2 | ||
|
|
888402a4bf | ||
|
|
1134f610fd | ||
|
|
8ae4304c7d | ||
|
|
357092ec44 | ||
|
|
70a5c76d79 | ||
|
|
7a4db8ea23 | ||
|
|
223b160c0c | ||
|
|
30c1771d29 | ||
|
|
b3b7b9bbab | ||
|
|
be040cd6ea | ||
|
|
c6665ec2e6 | ||
|
|
fd16ef1e4d | ||
|
|
39557fc452 | ||
|
|
408397a639 | ||
|
|
d4a2500204 | ||
|
|
e74d9e56cf | ||
|
|
f3767ab4ac | ||
|
|
5d13f5f885 | ||
|
|
451d3fce05 | ||
|
|
ccb61e0f56 | ||
|
|
b6273adc57 | ||
|
|
0bf7bba6ba | ||
|
|
7090e0bae2 | ||
|
|
c75cb0b8e3 | ||
|
|
3dbf22f670 | ||
|
|
f26cbdc257 | ||
|
|
6b4adccee5 | ||
|
|
c2a8286022 | ||
|
|
4145887a9b | ||
|
|
9f4b834abc | ||
|
|
8fcc314f09 | ||
|
|
94a7d02ab1 | ||
|
|
ad2943263c | ||
|
|
5210ac3a78 | ||
|
|
0e9600a7bf | ||
|
|
eccba09452 | ||
|
|
c8a830ecde | ||
|
|
aed64d16f6 | ||
|
|
d16f6167f6 | ||
|
|
77d59248e5 | ||
|
|
a0e05f8af6 | ||
|
|
9b8a47c8b8 | ||
|
|
b3d692276c | ||
|
|
55543e12f6 | ||
|
|
1e16185c02 | ||
|
|
cd900e24bd | ||
|
|
0dbedc07ce | ||
|
|
f71877b7fc | ||
|
|
f69e270e4d | ||
|
|
533939cae4 | ||
|
|
91ec5fd78c | ||
|
|
0056fb447b | ||
|
|
20c4d12e98 | ||
|
|
e13c567e84 | ||
|
|
9fef97a7c6 | ||
|
|
e68a995376 | ||
|
|
6abdb40ef5 | ||
|
|
43cc06b0a1 | ||
|
|
d17476cd75 | ||
|
|
5c3bfd2a71 | ||
|
|
033b8d70e7 | ||
|
|
bd22c2afc9 | ||
|
|
b355733f53 | ||
|
|
e1f924c4ce | ||
|
|
8038f4e173 | ||
|
|
5c55219d45 | ||
|
|
bfd37af467 | ||
|
|
b2509e120c | ||
|
|
e2339acd09 | ||
|
|
c15b4fa03c | ||
|
|
c4aa2e0484 | ||
|
|
361eeb7159 | ||
|
|
0109e1806f | ||
|
|
30aadac099 | ||
|
|
0458f1b2dc | ||
|
|
e006ca3feb | ||
|
|
1f31ee2ea1 | ||
|
|
2d37b0df77 | ||
|
|
4133e5ac4d | ||
|
|
0fd3d0fe71 | ||
|
|
d0685e99ad | ||
|
|
c6fd5bc864 | ||
|
|
9fa935099f | ||
|
|
83b5a325e3 | ||
|
|
97e12c5003 | ||
|
|
e6db8340f2 | ||
|
|
3cf9caa5d3 | ||
|
|
2ffd68ace7 | ||
|
|
0231be63b4 | ||
|
|
fae8bc254e | ||
|
|
1d5c700fa2 | ||
|
|
e61775d5c1 | ||
|
|
e767c6a68d | ||
|
|
832235411f | ||
|
|
1f0f7b752f | ||
|
|
3117eceb72 | ||
|
|
c1b39782fd | ||
|
|
860cfc3227 | ||
|
|
45859a07dd | ||
|
|
04fb8efc0d | ||
|
|
fdb8a3720b | ||
|
|
5638d68894 | ||
|
|
f64042280a | ||
|
|
50060cdc8d | ||
|
|
4499f58e3d | ||
|
|
918e4a5a89 | ||
|
|
15a86fd796 | ||
|
|
4126d20f1c | ||
|
|
ea3edf83f8 | ||
|
|
9a42819b56 | ||
|
|
3e4ba28700 | ||
|
|
9014ffcc28 | ||
|
|
48f4bcf88c | ||
|
|
b7dfb3697e | ||
|
|
475a5be351 | ||
|
|
8254d8f5cc | ||
|
|
6f0f4755ef | ||
|
|
910a35dedc | ||
|
|
e694bd8c21 | ||
|
|
29cf384c28 | ||
|
|
492288f437 | ||
|
|
34e4f7e0fc | ||
|
|
f6f3bbcce6 | ||
|
|
16054893ed | ||
|
|
f6038d2c39 | ||
|
|
8d13b51271 | ||
|
|
83e1f365c2 | ||
|
|
146e1aeb67 | ||
|
|
f9b2920984 | ||
|
|
2c01b214a7 | ||
|
|
fdab45e5ce | ||
|
|
9d2cf18543 | ||
|
|
2206ab1d35 | ||
|
|
ecd2c80dce | ||
|
|
3387df491a | ||
|
|
b6974e0c77 | ||
|
|
31751cbd79 | ||
|
|
993da5a392 | ||
|
|
72455209bb | ||
|
|
803aa0b70d | ||
|
|
954d86337c | ||
|
|
38a58d62f3 | ||
|
|
e67b39a57b | ||
|
|
148b67ac3f | ||
|
|
d261cb3b6b | ||
|
|
169a6c51b4 | ||
|
|
245ad644ff | ||
|
|
4fdce0d126 | ||
|
|
a542bc7a5a | ||
|
|
3164919923 | ||
|
|
8085311eb6 | ||
|
|
3887a65961 | ||
|
|
b229c6156a | ||
|
|
c45298544e | ||
|
|
7bb9d3fc3d | ||
|
|
8607df5a9c | ||
|
|
c4150473fc | ||
|
|
172b2f74e0 | ||
|
|
9586f71dc2 | ||
|
|
25692d180f | ||
|
|
ae047037dc | ||
|
|
265106034b | ||
|
|
dd0a4df914 | ||
|
|
b0ae40c264 | ||
|
|
ad95815043 | ||
|
|
f68522ec0d | ||
|
|
b831e57351 | ||
|
|
51166786ee | ||
|
|
909e7906ff | ||
|
|
e185d5f0e7 | ||
|
|
ce8edf621b | ||
|
|
e58b512876 | ||
|
|
d1754f6d1b | ||
|
|
ff2f1b7424 | ||
|
|
fb1838a2f0 | ||
|
|
d7b05063a4 | ||
|
|
f64a42d61a | ||
|
|
c1994e89a5 | ||
|
|
f37de1ad2f | ||
|
|
e1ff6f8590 | ||
|
|
a5dd22eb4d | ||
|
|
19cde63505 | ||
|
|
754d4f4f62 | ||
|
|
e433230573 | ||
|
|
f8927396d3 | ||
|
|
60be99fbb2 | ||
|
|
0c508c5ba4 | ||
|
|
ea6067ab3f | ||
|
|
9d0fa84277 | ||
|
|
a6835d3b14 | ||
|
|
9ff565f772 | ||
|
|
5d41b20bae | ||
|
|
03de0d5d2e | ||
|
|
2937acdc66 | ||
|
|
6fd09e99e2 | ||
|
|
290e14689d | ||
|
|
89c937089b | ||
|
|
0e02febe76 | ||
|
|
771f822e5f | ||
|
|
e8936551c0 | ||
|
|
ea0f6dfc54 | ||
|
|
abeddd360e | ||
|
|
c209d195bf | ||
|
|
35c46d320c | ||
|
|
30621568ab | ||
|
|
403c4f4499 | ||
|
|
884bba0088 | ||
|
|
2b52edd5b7 | ||
|
|
a4aed96784 | ||
|
|
4bdfd56264 | ||
|
|
31f0b07325 | ||
|
|
3f08f3a7f4 | ||
|
|
93263e7567 | ||
|
|
69cf62d2ca | ||
|
|
bb353e5fde | ||
|
|
2dceff1218 | ||
|
|
5ea8a8ef82 | ||
|
|
03a7a3303c | ||
|
|
2beb0b20ca | ||
|
|
24eea02e0d | ||
|
|
15ab9c72d3 | ||
|
|
c957d77fe0 | ||
|
|
7697018ca4 | ||
|
|
3980a7b2a7 | ||
|
|
035bb56386 | ||
|
|
837b03fff3 | ||
|
|
d3dec72831 | ||
|
|
3d78f68d94 | ||
|
|
faa43d4df8 | ||
|
|
78917afa1a | ||
|
|
4b53d39e3e | ||
|
|
02db07cd25 | ||
|
|
19fb6c8c34 | ||
|
|
0c25b2df92 | ||
|
|
6a543e4557 | ||
|
|
846527546a | ||
|
|
c8cdb2b311 | ||
|
|
96ff3d532d | ||
|
|
8ebba9de86 | ||
|
|
c4e71011ee | ||
|
|
e71ad4bfba | ||
|
|
05a5a69128 | ||
|
|
bb83cd2f39 | ||
|
|
df26171ff1 | ||
|
|
da937dc4e3 | ||
|
|
bb9508ad96 | ||
|
|
41fed7d6a2 | ||
|
|
f441e9984d | ||
|
|
05c6155f37 | ||
|
|
3c096325bd | ||
|
|
d06a352df5 | ||
|
|
ba7b1bb89e | ||
|
|
3dcfa57b70 | ||
|
|
cc13ca1c3f | ||
|
|
aac67ebf83 | ||
|
|
b51e1cfc6f | ||
|
|
f0508cdcc3 | ||
|
|
9ed2dc7b46 | ||
|
|
0e568a3fca | ||
|
|
7f3606ee81 | ||
|
|
b22d43860a | ||
|
|
0f9b339f01 | ||
|
|
cd1e9c1740 | ||
|
|
aec1ce53fc | ||
|
|
aae129be6a | ||
|
|
b906fe0fc3 | ||
|
|
f0f1537e9c | ||
|
|
7b7e77d497 | ||
|
|
9ac705cd88 | ||
|
|
01d9574ddf | ||
|
|
8121167d5e | ||
|
|
dde4e12ce1 | ||
|
|
6cd32400ae | ||
|
|
8fa71ccad4 | ||
|
|
0f47bff5cd | ||
|
|
f459f1f12d | ||
|
|
65167cc290 | ||
|
|
bc7300c393 | ||
|
|
d8450202fe | ||
|
|
41d2bcc34f | ||
|
|
0e1589013a | ||
|
|
39f81617e1 | ||
|
|
b394ef6de1 | ||
|
|
177906e2ac | ||
|
|
59f6b20129 | ||
|
|
51998e820d | ||
|
|
e803b56716 | ||
|
|
fa8b1c176b | ||
|
|
2598787602 | ||
|
|
003fa62996 | ||
|
|
798c21955e | ||
|
|
fe6185af4b | ||
|
|
7bacefa442 | ||
|
|
04e187c297 | ||
|
|
9f2ffc3276 | ||
|
|
a9a4cf6fca | ||
|
|
a563316e22 | ||
|
|
4b6f55c31d | ||
|
|
21a8fad17a | ||
|
|
7586df9d3f | ||
|
|
1d4afa5d27 | ||
|
|
720d9b924e | ||
|
|
9f56669f2a | ||
|
|
fc541016c6 | ||
|
|
5eefe9ad1e | ||
|
|
1d065a7672 | ||
|
|
101f5f7781 | ||
|
|
af7c6d360f | ||
|
|
8751e6e5ba | ||
|
|
93004a8125 | ||
|
|
adf40e1d56 | ||
|
|
364cfe0131 | ||
|
|
1514527ef3 | ||
|
|
680024234d | ||
|
|
2a3660f2d1 | ||
|
|
2041d1213a | ||
|
|
42a1fe9bd1 | ||
|
|
002469d523 | ||
|
|
5be4af1305 | ||
|
|
0b241438e1 | ||
|
|
61649ab2b8 | ||
|
|
848ea999c5 | ||
|
|
dfa82870fb | ||
|
|
e05ac7ef34 | ||
|
|
ad2334bffc | ||
|
|
17adde99fa | ||
|
|
4789d82c4e | ||
|
|
0567e2d22b | ||
|
|
2e0592b0a6 | ||
|
|
7f6d234b4c | ||
|
|
0436de316b | ||
|
|
e16d643d2a | ||
|
|
bdec22cf3b | ||
|
|
b38df27dce | ||
|
|
b95f556d8f | ||
|
|
851a4c977c | ||
|
|
7bffd461d1 | ||
|
|
9a3b4f7863 | ||
|
|
673a38ddc8 | ||
|
|
a27b8bf213 | ||
|
|
36e6f10b37 | ||
|
|
fde10d7f55 | ||
|
|
6b44b2f429 | ||
|
|
5e9018e0fd | ||
|
|
185f8066ae | ||
|
|
6388f7b29c | ||
|
|
4aa2c9d51d | ||
|
|
ef9256f0b0 | ||
|
|
28d78e40f9 | ||
|
|
89554a82eb | ||
|
|
ae99e82ad1 | ||
|
|
5ea3d01b8d | ||
|
|
aa2bd79b99 | ||
|
|
44ee35b885 | ||
|
|
22b79a8c22 | ||
|
|
65bbd537e6 | ||
|
|
34387d7bc0 | ||
|
|
ca38204313 | ||
|
|
b7083eca2e | ||
|
|
6bb8b428dc | ||
|
|
677142d0c9 | ||
|
|
d1b66e365a | ||
|
|
50154c02ce | ||
|
|
04375d4fcf | ||
|
|
9c1ff296bb | ||
|
|
0b3acb06b5 | ||
|
|
b2cdccedd6 | ||
|
|
7ebefa7b85 | ||
|
|
c7b5baa185 | ||
|
|
6d08e7a8b0 | ||
|
|
0da2b12646 | ||
|
|
a0693483dc | ||
|
|
29826a9f08 | ||
|
|
36a045020f | ||
|
|
40c2b774aa | ||
|
|
8422b2b4aa | ||
|
|
ae334c4860 | ||
|
|
722f36121d | ||
|
|
529092a4ed | ||
|
|
e7068020d5 | ||
|
|
08acecf37b | ||
|
|
b200ca5ad5 | ||
|
|
e564952148 | ||
|
|
f4ad2a2293 | ||
|
|
854bbf26c2 | ||
|
|
ec5a670ea6 | ||
|
|
276add9163 | ||
|
|
de977f4818 | ||
|
|
a4827fc992 | ||
|
|
9a002bf172 | ||
|
|
9a7f3e2d8a | ||
|
|
e7546a7575 | ||
|
|
434719285b | ||
|
|
5bc9ba4641 | ||
|
|
74dd13abd5 | ||
|
|
ead755aa86 | ||
|
|
1f46a8b91b | ||
|
|
eb77c2f6f6 | ||
|
|
c5fe615be5 | ||
|
|
f5504e11ac | ||
|
|
e88a1a52f9 | ||
|
|
b86d54ea9f | ||
|
|
b6e2ed14db | ||
|
|
c513868afa | ||
|
|
3f7664f743 | ||
|
|
e654b951ed | ||
|
|
b5c7556abe | ||
|
|
53e3619140 | ||
|
|
e191988b81 | ||
|
|
bb7fd9423b | ||
|
|
c10c6ee28d | ||
|
|
3c64733e93 | ||
|
|
08cb045f2e | ||
|
|
7bf854fe0b | ||
|
|
f2a1e11b85 | ||
|
|
9b07912b7f | ||
|
|
e1cec9882a | ||
|
|
1ff9c1a84b | ||
|
|
0035825f33 | ||
|
|
1ec73b1b33 | ||
|
|
ed83f4558e | ||
|
|
b18ec7605a | ||
|
|
f96bc0776d | ||
|
|
629bdcd55d | ||
|
|
829fd907a1 | ||
|
|
d7fe321f36 | ||
|
|
517432319e | ||
|
|
edef9f1b23 | ||
|
|
617730ab76 | ||
|
|
7c17d041f4 | ||
|
|
9295abb80e | ||
|
|
4c3192f116 | ||
|
|
9c6a2eb85a | ||
|
|
bcbc8a542f | ||
|
|
a915442efc | ||
|
|
103631a14b | ||
|
|
e9d7a24cbf | ||
|
|
cc977e441a | ||
|
|
add9bae018 | ||
|
|
77d157ab8e | ||
|
|
e42bc94329 | ||
|
|
b4bf5f998e | ||
|
|
dc785e9dac | ||
|
|
8f5f95b04e | ||
|
|
c86839ed41 | ||
|
|
8b6e0f0de7 | ||
|
|
a65243e4bb | ||
|
|
ac028be84e | ||
|
|
efd5b5b1da | ||
|
|
4be618bc93 | ||
|
|
7b6d5a0cc9 | ||
|
|
f367d5e675 | ||
|
|
f9b7894c4d | ||
|
|
354bbb485b | ||
|
|
8dc5dbd547 | ||
|
|
e04793d2eb | ||
|
|
db65c14733 | ||
|
|
f10c8b229f | ||
|
|
4655d8237f | ||
|
|
78f4f35ca3 | ||
|
|
3a01a05a08 | ||
|
|
1738c710cb | ||
|
|
d07783a453 | ||
|
|
1ce331f163 | ||
|
|
586f95bc6d | ||
|
|
5620aec5f2 | ||
|
|
c1dfec20f6 | ||
|
|
7fef81bdef | ||
|
|
0f2e905672 | ||
|
|
a57a4e7350 | ||
|
|
b57a6e982a | ||
|
|
39736ef0d4 | ||
|
|
f7e5f0b567 | ||
|
|
b6078d5272 | ||
|
|
1ed1cd33e8 | ||
|
|
a4a2500725 | ||
|
|
3fb44ec9dd | ||
|
|
2a96575b4d | ||
|
|
dcf29ec63e | ||
|
|
a743605bd3 | ||
|
|
75dc80eb09 | ||
|
|
ac16d9d900 | ||
|
|
736d26c232 | ||
|
|
8985dfc5eb | ||
|
|
bb80ef067a | ||
|
|
bdd9751f0e | ||
|
|
965aac6ad5 | ||
|
|
e3858373d1 | ||
|
|
fcdfae88d7 | ||
|
|
7d5a85e26f | ||
|
|
b8b2c2eba3 | ||
|
|
c6a3280d69 | ||
|
|
7f9368c415 | ||
|
|
add764e3f0 | ||
|
|
a3431cd51e | ||
|
|
9772d43235 | ||
|
|
2e29e369f5 | ||
|
|
9f6ce81229 | ||
|
|
d67954de3f | ||
|
|
d04f93d45c | ||
|
|
ef70209ba8 | ||
|
|
f127cfc46a | ||
|
|
ec444e5bf3 | ||
|
|
32f690e9d0 | ||
|
|
9089b630ed | ||
|
|
0c6971ff5f | ||
|
|
59e92245de | ||
|
|
9894954233 | ||
|
|
6e7505abd5 | ||
|
|
9df381ec4c | ||
|
|
be726183cb | ||
|
|
be5e2d8c33 | ||
|
|
9da7321a19 | ||
|
|
cc7d95b805 | ||
|
|
5376dbffc0 | ||
|
|
cddefd98d3 | ||
|
|
5ae0e55f7e | ||
|
|
856c36b85a | ||
|
|
a3bc717a5b | ||
|
|
6fa198a175 | ||
|
|
9596f48fed | ||
|
|
11b1c81633 | ||
|
|
212f33afee | ||
|
|
3fab15d086 | ||
|
|
2f1dd79162 | ||
|
|
e00ab01235 | ||
|
|
60f0e297e3 | ||
|
|
93c791e16f | ||
|
|
6da8caaa2b | ||
|
|
38ffd7d6ba | ||
|
|
ff4f56392d | ||
|
|
618b67ca2f | ||
|
|
a856a3ef6f | ||
|
|
573284c480 | ||
|
|
d4712266ff | ||
|
|
e4f542b060 | ||
|
|
1b68e8bf0e | ||
|
|
c8d464ded7 | ||
|
|
12ab5ace9c | ||
|
|
a2126c7b15 | ||
|
|
cba2ad5333 | ||
|
|
8700c41f5e | ||
|
|
a88fed283a | ||
|
|
130ffddf48 | ||
|
|
f84b612d7b | ||
|
|
e1ac22067a | ||
|
|
60c3b76ee9 | ||
|
|
fa8552e86f | ||
|
|
ecf1a40a5e | ||
|
|
ecfeae6ad9 | ||
|
|
3544c3f5b8 | ||
|
|
d8f3a3f5be | ||
|
|
d6849c45fe | ||
|
|
eaf663794e | ||
|
|
dbbd4fe47f | ||
|
|
abab7dc874 | ||
|
|
11ddfc511b | ||
|
|
3e50f3dd33 | ||
|
|
bf5becad82 | ||
|
|
f191ce823a | ||
|
|
b03fed979f | ||
|
|
91de41b782 | ||
|
|
ff1cfe269f | ||
|
|
2641a40142 | ||
|
|
584d869729 | ||
|
|
8b9b86a68d | ||
|
|
b7f5631ad0 | ||
|
|
038413be88 | ||
|
|
4508745feb | ||
|
|
f9fa1733b0 | ||
|
|
d50dff4a6e | ||
|
|
2852722b50 | ||
|
|
1ef076bb9b | ||
|
|
8ad53256c2 | ||
|
|
f51155a5df | ||
|
|
75f9824095 | ||
|
|
9678ef3dd4 | ||
|
|
4d945cf1e3 | ||
|
|
8f05de7004 | ||
|
|
72388abd57 | ||
|
|
5801c8602e | ||
|
|
eb77f67d28 | ||
|
|
ba895270fa | ||
|
|
cd88659351 | ||
|
|
eabead4768 | ||
|
|
9cb0cf210a | ||
|
|
d181241a63 | ||
|
|
b3edb82ffd | ||
|
|
eb5ed2bdf9 | ||
|
|
c132ccd141 | ||
|
|
fb2827e9ab | ||
|
|
bb89bf68ef | ||
|
|
97d67d58d5 | ||
|
|
3235f90876 | ||
|
|
227b2513b4 | ||
|
|
5952fdccb8 | ||
|
|
4874748aa2 | ||
|
|
030ea269b0 | ||
|
|
d187a497f9 | ||
|
|
efc2efac84 | ||
|
|
3378744a5c | ||
|
|
71ac461929 | ||
|
|
039da531c4 | ||
|
|
91e080d962 | ||
|
|
b78cf9f2c5 | ||
|
|
af68053195 | ||
|
|
2dd1e567cf | ||
|
|
33400ed7cc | ||
|
|
fe2e01938a | ||
|
|
fccd119a1f | ||
|
|
b1dee5ae7c | ||
|
|
3e178a7293 | ||
|
|
193407d819 | ||
|
|
25419dc8e8 | ||
|
|
cec27d7a44 | ||
|
|
881f0e04a0 | ||
|
|
5ee51c8f9a | ||
|
|
050f3990c3 | ||
|
|
b11ae9e5dd | ||
|
|
c7ef79be90 | ||
|
|
9c3fc69176 | ||
|
|
18df9d66bb | ||
|
|
a43625c5e8 | ||
|
|
a4d9d7041c | ||
|
|
96eabebc15 | ||
|
|
3819df57d8 | ||
|
|
0fee7b0613 | ||
|
|
1a17f54354 | ||
|
|
750231eb3c | ||
|
|
1bb84b7296 | ||
|
|
6d9ef397ee | ||
|
|
64d07a2811 | ||
|
|
e4949b6491 | ||
|
|
71e7df3038 | ||
|
|
47df6fe2bc | ||
|
|
ec08faf205 | ||
|
|
76e86cbdd1 | ||
|
|
f1b072b9a4 | ||
|
|
e792e8fd1e | ||
|
|
bc8b3f504c | ||
|
|
0bcbfda276 | ||
|
|
db029882ec | ||
|
|
a1cc17094d | ||
|
|
1c763ccce3 | ||
|
|
36fd5e7d01 | ||
|
|
66cf9c1ac7 | ||
|
|
0b9b67d603 | ||
|
|
520fb62088 |
15
.github/dependabot.yml
vendored
Normal file
15
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/src"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/src/pretix/static/npm_dir"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
3
.github/workflows/docs.yml
vendored
3
.github/workflows/docs.yml
vendored
@@ -33,7 +33,8 @@ jobs:
|
||||
- name: Install system packages
|
||||
run: sudo apt update && sudo apt install enchant hunspell aspell-en
|
||||
- name: Install Dependencies
|
||||
run: pip3 install -Ur doc/requirements.txt
|
||||
run: pip3 install -Ur requirements.txt
|
||||
working-directory: ./doc
|
||||
- name: Spellcheck docs
|
||||
run: make spelling
|
||||
working-directory: ./doc
|
||||
|
||||
6
.github/workflows/strings.yml
vendored
6
.github/workflows/strings.yml
vendored
@@ -31,7 +31,8 @@ jobs:
|
||||
- name: Install system packages
|
||||
run: sudo apt update && sudo apt install gettext
|
||||
- name: Install Dependencies
|
||||
run: pip3 install -Ur src/requirements.txt
|
||||
run: pip3 install -e ".[dev]"
|
||||
working-directory: ./src
|
||||
- name: Compile messages
|
||||
run: python manage.py compilemessages
|
||||
working-directory: ./src
|
||||
@@ -56,7 +57,8 @@ jobs:
|
||||
- name: Install system packages
|
||||
run: sudo apt update && sudo apt install enchant hunspell hunspell-de-de aspell-en aspell-de
|
||||
- name: Install Dependencies
|
||||
run: pip3 install -Ur src/requirements/dev.txt
|
||||
run: pip3 install -e ".[dev]"
|
||||
working-directory: ./src
|
||||
- name: Spellcheck translations
|
||||
run: potypo
|
||||
working-directory: ./src
|
||||
|
||||
6
.github/workflows/style.yml
vendored
6
.github/workflows/style.yml
vendored
@@ -29,7 +29,8 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install Dependencies
|
||||
run: pip3 install -Ur src/requirements/dev.txt
|
||||
run: pip3 install -e ".[dev]" mysqlclient psycopg2-binary
|
||||
working-directory: ./src
|
||||
- name: Run isort
|
||||
run: isort -c .
|
||||
working-directory: ./src
|
||||
@@ -49,7 +50,8 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install Dependencies
|
||||
run: pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt
|
||||
run: pip3 install -e ".[dev]" mysqlclient psycopg2-binary
|
||||
working-directory: ./src
|
||||
- name: Run flake8
|
||||
run: flake8 .
|
||||
working-directory: ./src
|
||||
|
||||
17
.github/workflows/tests.yml
vendored
17
.github/workflows/tests.yml
vendored
@@ -18,17 +18,17 @@ jobs:
|
||||
name: Tests
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.6, 3.7, 3.8]
|
||||
python-version: ["3.7", "3.8", "3.9"]
|
||||
database: [sqlite, postgres, mysql]
|
||||
exclude:
|
||||
- database: mysql
|
||||
python-version: 3.7
|
||||
- database: sqlite
|
||||
python-version: 3.7
|
||||
python-version: "3.8"
|
||||
- database: mysql
|
||||
python-version: 3.6
|
||||
python-version: "3.9"
|
||||
- database: sqlite
|
||||
python-version: 3.6
|
||||
python-version: "3.7"
|
||||
- database: sqlite
|
||||
python-version: "3.8"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: getong/mariadb-action@v1.1
|
||||
@@ -55,9 +55,10 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install system dependencies
|
||||
run: sudo apt update && sudo apt install gettext mysql-client
|
||||
run: sudo apt update && sudo apt install gettext mariadb-client
|
||||
- name: Install Python dependencies
|
||||
run: pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt mysqlclient psycopg2-binary
|
||||
run: pip3 install -e ".[dev]" mysqlclient psycopg2-binary
|
||||
working-directory: ./src
|
||||
- name: Run checks
|
||||
run: python manage.py check
|
||||
working-directory: ./src
|
||||
|
||||
@@ -5,8 +5,8 @@ tests:
|
||||
- virtualenv env
|
||||
- source env/bin/activate
|
||||
- pip install -U pip wheel setuptools
|
||||
- XDG_CACHE_HOME=/cache pip3 install -r src/requirements.txt --no-use-pep517 -Ur src/requirements/dev.txt
|
||||
- cd src
|
||||
- XDG_CACHE_HOME=/cache pip3 install -e ".[dev]"
|
||||
- python manage.py check
|
||||
- make all compress
|
||||
- py.test --reruns 3 -n 3 tests
|
||||
@@ -21,8 +21,8 @@ pypi:
|
||||
- virtualenv env
|
||||
- source env/bin/activate
|
||||
- pip install -U pip wheel setuptools check-manifest twine
|
||||
- XDG_CACHE_HOME=/cache pip3 install -Ur src/requirements.txt -r src/requirements/dev.txt
|
||||
- cd src
|
||||
- XDG_CACHE_HOME=/cache pip3 install -e ".[dev]"
|
||||
- python setup.py sdist
|
||||
- pip install dist/pretix-*.tar.gz
|
||||
- python -m pretix migrate
|
||||
|
||||
19
Dockerfile
19
Dockerfile
@@ -1,9 +1,9 @@
|
||||
FROM python:3.8
|
||||
FROM python:3.9-bullseye
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
default-libmysqlclient-dev \
|
||||
libmariadb-dev \
|
||||
gettext \
|
||||
git \
|
||||
libffi-dev \
|
||||
@@ -15,8 +15,7 @@ RUN apt-get update && \
|
||||
libxslt1-dev \
|
||||
locales \
|
||||
nginx \
|
||||
python-dev \
|
||||
python-virtualenv \
|
||||
python3-virtualenv \
|
||||
python3-dev \
|
||||
sudo \
|
||||
supervisor \
|
||||
@@ -41,17 +40,14 @@ ENV LC_ALL=C.UTF-8 \
|
||||
DJANGO_SETTINGS_MODULE=production_settings
|
||||
|
||||
# To copy only the requirements files needed to install from PIP
|
||||
COPY src/requirements /pretix/src/requirements
|
||||
COPY src/requirements.txt /pretix/src
|
||||
COPY src/setup.py /pretix/src/setup.py
|
||||
RUN pip3 install -U \
|
||||
pip \
|
||||
setuptools \
|
||||
wheel && \
|
||||
cd /pretix/src && \
|
||||
pip3 install \
|
||||
-r requirements.txt \
|
||||
-r requirements/memcached.txt \
|
||||
-r requirements/mysql.txt \
|
||||
PRETIX_DOCKER_BUILD=TRUE pip3 install \
|
||||
-e ".[memcached,mysql]" \
|
||||
gunicorn django-extensions ipython && \
|
||||
rm -rf ~/.cache/pip
|
||||
|
||||
@@ -60,10 +56,11 @@ COPY deployment/docker/supervisord /etc/supervisord
|
||||
COPY deployment/docker/supervisord.all.conf /etc/supervisord.all.conf
|
||||
COPY deployment/docker/supervisord.web.conf /etc/supervisord.web.conf
|
||||
COPY deployment/docker/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY deployment/docker/nginx-max-body-size.conf /etc/nginx/conf.d/nginx-max-body-size.conf
|
||||
COPY deployment/docker/production_settings.py /pretix/src/production_settings.py
|
||||
COPY src /pretix/src
|
||||
|
||||
RUN cd /pretix/src && pip3 install .
|
||||
RUN cd /pretix/src && python setup.py install
|
||||
|
||||
RUN chmod +x /usr/local/bin/pretix && \
|
||||
rm /etc/nginx/sites-enabled/default && \
|
||||
|
||||
1
deployment/docker/nginx-max-body-size.conf
Normal file
1
deployment/docker/nginx-max-body-size.conf
Normal file
@@ -0,0 +1 @@
|
||||
client_max_body_size 100M;
|
||||
@@ -16,7 +16,6 @@ http {
|
||||
charset utf-8;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
client_max_body_size 100M;
|
||||
|
||||
log_format private '[$time_local] $host "$request" $status $body_bytes_sent';
|
||||
|
||||
@@ -66,9 +65,18 @@ http {
|
||||
access_log off;
|
||||
expires 365d;
|
||||
add_header Cache-Control "public";
|
||||
add_header Access-Control-Allow-Origin "*";
|
||||
gzip on;
|
||||
}
|
||||
location / {
|
||||
proxy_pass http://unix:/tmp/pretix.sock:/;
|
||||
# Very important:
|
||||
# proxy_pass http://unix:/tmp/pretix.sock:;
|
||||
# is not the same as
|
||||
# proxy_pass http://unix:/tmp/pretix.sock:/;
|
||||
# In the latter case, nginx will apply its URL parsing, in the former it doesn't.
|
||||
# There are situations in which pretix' API will deal with "file names" containing %2F%2F, which
|
||||
# nginx will normalize to %2F, which can break ticket validation.
|
||||
proxy_pass http://unix:/tmp/pretix.sock:;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
}
|
||||
|
||||
@@ -65,6 +65,9 @@ Example::
|
||||
A comma-separated list of plugins that are not available even though they are installed.
|
||||
Defaults to an empty string.
|
||||
|
||||
``plugins_show_meta``
|
||||
Whether to show authors and versions of plugins, defaults to ``on``.
|
||||
|
||||
``auth_backends``
|
||||
A comma-separated list of available auth backends. Defaults to ``pretix.base.auth.NativeAuthBackend``.
|
||||
|
||||
@@ -220,12 +223,30 @@ Example::
|
||||
``user``, ``password``
|
||||
The SMTP user data to use for the connection. Empty by default.
|
||||
|
||||
``tls``, ``ssl``
|
||||
Use STARTTLS or SSL for the SMTP connection. Off by default.
|
||||
|
||||
``from``
|
||||
The email address to set as ``From`` header in outgoing emails by the system.
|
||||
Default: ``pretix@localhost``
|
||||
|
||||
``tls``, ``ssl``
|
||||
Use STARTTLS or SSL for the SMTP connection. Off by default.
|
||||
``from_notifications``
|
||||
The email address to set as ``From`` header in admin notification emails by the system.
|
||||
Defaults to the value of ``from``.
|
||||
|
||||
``from_organizers``
|
||||
The email address to set as ``From`` header in outgoing emails by the system sent on behalf of organizers.
|
||||
Defaults to the value of ``from``.
|
||||
|
||||
``custom_sender_verification_required``
|
||||
If this is on (the default), organizers need to verify email addresses they want to use as senders in their event.
|
||||
|
||||
``custom_sender_spf_string``
|
||||
If this is set to a valid SPF string, pretix will show a warning if organizers use a sender address from a domain
|
||||
that does not include this value.
|
||||
|
||||
``custom_smtp_allow_private_networks``
|
||||
If this is off (the default), custom SMTP servers cannot be private network addresses.
|
||||
|
||||
``admins``
|
||||
Comma-separated list of email addresses that should receive a report about every error code 500 thrown by pretix.
|
||||
@@ -282,7 +303,7 @@ You can use an existing memcached server as pretix's caching backend::
|
||||
``location``
|
||||
The location of memcached, either a host:port combination or a socket file.
|
||||
|
||||
If no memcached is configured, pretix will use Django's built-in local-memory caching method.
|
||||
If no memcached is configured, pretix will use redis for caching. If neither is configured, pretix will not use any caching.
|
||||
|
||||
.. note:: If you use memcached and you deploy pretix across multiple servers, you should use *one*
|
||||
shared memcached instance, not multiple ones, because cache invalidations would not be
|
||||
@@ -297,6 +318,12 @@ to speed up various operations::
|
||||
[redis]
|
||||
location=redis://127.0.0.1:6379/1
|
||||
sessions=false
|
||||
sentinels=[
|
||||
["sentinel_host_1", 26379],
|
||||
["sentinel_host_2", 26379],
|
||||
["sentinel_host_3", 26379]
|
||||
]
|
||||
password=password
|
||||
|
||||
``location``
|
||||
The location of redis, as a URL of the form ``redis://[:password]@localhost:6379/0``
|
||||
@@ -305,13 +332,34 @@ to speed up various operations::
|
||||
``session``
|
||||
When this is set to ``True``, redis will be used as the session storage.
|
||||
|
||||
``sentinels``
|
||||
Configures redis sentinels to use.
|
||||
If you don't want to use redis sentinels, you should omit this option.
|
||||
If this is set, redis via sentinels will be used instead of plain redis.
|
||||
In this case the location should be of the form ``redis://my_master/0``.
|
||||
The ``sentinels`` variable should be a json serialized list of sentinels,
|
||||
each being a list with the two elements hostname and port.
|
||||
You cannot provide a password within the location when using sentinels.
|
||||
Note that the configuration format requires you to either place the entire
|
||||
value on one line or make sure all values are indented by at least one space.
|
||||
|
||||
``password``
|
||||
If your redis setup doesn't require a password or you already specified it in the location you can omit this option.
|
||||
If this is set it will be passed to redis as the connection option PASSWORD.
|
||||
|
||||
If redis is not configured, pretix will store sessions and locks in the database. If memcached
|
||||
is configured, memcached will be used for caching instead of redis.
|
||||
|
||||
Translations
|
||||
------------
|
||||
|
||||
pretix comes with a number of translations. Some of them are marked as "incubating", which means
|
||||
pretix comes with a number of translations. All languages are enabled by default. If you want to limit
|
||||
the languages available in your installation, you can enable a set of languages like this::
|
||||
|
||||
[languages]
|
||||
enabled=en,de
|
||||
|
||||
Some of the languages them are marked as "incubating", which means
|
||||
they can usually only be selected in development mode. If you want to use them nevertheless, you
|
||||
can activate them like this::
|
||||
|
||||
@@ -337,11 +385,22 @@ an AMQP server (e.g. RabbitMQ) as a broker and redis or your database as a resul
|
||||
[celery]
|
||||
broker=amqp://guest:guest@localhost:5672//
|
||||
backend=redis://localhost/0
|
||||
broker_transport_options="{}"
|
||||
backend_transport_options="{}"
|
||||
|
||||
RabbitMQ might be the better choice if you have a complex, multi-server, high-performance setup,
|
||||
but as you already should have a redis instance ready for session and lock storage, we recommend
|
||||
redis for convenience. See the `Celery documentation`_ for more details.
|
||||
|
||||
The two ``transport_options`` entries can be omitted in most cases.
|
||||
If they are present they need to be a valid JSON dictionary.
|
||||
For possible entries in that dictionary see the `Celery documentation`_.
|
||||
|
||||
To use redis with sentinels set the broker or backend to ``sentinel://sentinel_host_1:26379;sentinal_host_2:26379/0``
|
||||
and the respective transport_options to ``{"master_name":"mymaster"}``.
|
||||
If your redis instances behind the sentinel have a password use ``sentinel://:my_password@sentinel_host_1:26379;sentinal_host_2:26379/0``.
|
||||
If your redis sentinels themselves have a password set the transport_options to ``{"master_name":"mymaster","sentinel_kwargs":{"password":"my_password"}}``.
|
||||
|
||||
Sentry
|
||||
------
|
||||
|
||||
@@ -396,3 +455,21 @@ pretix can make use of some external tools if they are installed. Currently, the
|
||||
|
||||
.. _Python documentation: https://docs.python.org/3/library/configparser.html?highlight=configparser#supported-ini-file-structure
|
||||
.. _Celery documentation: http://docs.celeryproject.org/en/latest/userguide/configuration.html
|
||||
|
||||
Maximum upload file sizes
|
||||
-------------------------
|
||||
|
||||
You can configure the maximum file size for uploading various files::
|
||||
|
||||
[pretix_file_upload]
|
||||
; Max upload size for images in MiB, defaults to 10 MiB
|
||||
max_size_image = 12
|
||||
; Max upload size for favicons in MiB, defaults to 1 MiB
|
||||
max_size_favicon = 2
|
||||
; Max upload size for email attachments of manually sent emails in MiB, defaults to 10 MiB
|
||||
max_size_email_attachment = 15
|
||||
; Max upload size for email attachments of automatically sent emails in MiB, defaults to 1 MiB
|
||||
max_size_email_auto_attachment = 2
|
||||
; Max upload size for other files in MiB, defaults to 10 MiB
|
||||
; This includes all file upload type order questions
|
||||
max_size_other = 100
|
||||
|
||||
40
doc/admin/errors.rst
Normal file
40
doc/admin/errors.rst
Normal file
@@ -0,0 +1,40 @@
|
||||
.. _`admin-errors`:
|
||||
|
||||
Dealing with errors
|
||||
===================
|
||||
|
||||
If you encounter an error in pretix, please follow the following steps to debug it:
|
||||
|
||||
* If the error message is shown on a **white page** and the last line of the error includes "nginx", the error is not with pretix
|
||||
directly but with your nginx webserver. This might mean that pretix is not running, but it could also be something else.
|
||||
Please first check your nginx error log. The default location is ``/var/log/nginx/error.log``.
|
||||
|
||||
* If it turns out pretix is not running, check the output of ``docker logs pretix`` for a docker installation and
|
||||
``journalctl -u pretix-web.service`` for a manual installation.
|
||||
|
||||
* If the error message is an "**Internal Server Error**" in purple pretix design, please check pretix' log file which by default is at
|
||||
``/var/pretix-data/logs/pretix.log`` if you installed with docker and ``/var/pretix/data/logs/pretix.log`` otherwise. If you don't
|
||||
know how to interpret it, open a discussion on GitHub with the relevant parts of the log file.
|
||||
|
||||
* If the error message includes ``/usr/bin/env: ‘node’: No such file or directory``, you forgot to install ``node.js``
|
||||
|
||||
* If the error message includes ``OfflineGenerationError``, you might have forgot to run the ``rebuild`` step after a pretix update
|
||||
or plugin installation.
|
||||
|
||||
* If the error message mentions your database server or redis server, make sure these are running and accessible.
|
||||
|
||||
* If pretix loads fine but certain actions (creating carts, orders, or exports, downloading tickets, sending emails) **take forever**,
|
||||
``pretix-worker`` is not running. Check the output of ``docker logs pretix`` for a docker installation and
|
||||
``journalctl -u pretix-worker.service`` for a manual installation.
|
||||
|
||||
* If the page loads but all **styles are missing**, you probably forgot to update your nginx configuration file after an upgrade of your
|
||||
operating system's python version.
|
||||
|
||||
|
||||
If you are unable to debug the issue any further, please open a **discussion** on GitHub in our `Q&A Forum`_. Do **not** open an issue
|
||||
right away, since most things turn out not to be a bug in pretix but a mistake in your server configuration. Make sure to include
|
||||
relevant log excerpts in your question.
|
||||
|
||||
If you're a pretix Enterprise customer, you can also reach out to support@pretix.eu with your issue right away.
|
||||
|
||||
.. _Q&A Forum: https://github.com/pretix/pretix/discussions/categories/q-a
|
||||
@@ -9,7 +9,9 @@ This documentation is for everyone who wants to install pretix on a server.
|
||||
:maxdepth: 2
|
||||
|
||||
installation/index
|
||||
updates
|
||||
config
|
||||
maintainance
|
||||
scaling
|
||||
errors
|
||||
indexes
|
||||
|
||||
@@ -50,7 +50,7 @@ Here is the currently recommended set of commands::
|
||||
CREATE INDEX CONCURRENTLY pretix_addidx_orderpos_name
|
||||
ON pretixbase_orderposition
|
||||
USING gin (upper("attendee_name_cached") gin_trgm_ops);
|
||||
CREATE INDEX CONCURRENTLY pretix_addidx_orderpos_scret
|
||||
CREATE INDEX CONCURRENTLY pretix_addidx_orderpos_secret
|
||||
ON pretixbase_orderposition
|
||||
USING gin (upper("secret") gin_trgm_ops);
|
||||
CREATE INDEX CONCURRENTLY pretix_addidx_orderpos_email
|
||||
|
||||
@@ -36,8 +36,9 @@ Linux and firewalls, we recommend that you start with `ufw`_.
|
||||
SSL certificates can be obtained for free these days. We also *do not* provide support for HTTP-only
|
||||
installations except for evaluation purposes.
|
||||
|
||||
.. warning:: We recommend **PostgreSQL**. If you go for MySQL, make sure you run **MySQL 5.7 or newer** or
|
||||
**MariaDB 10.2.7 or newer**.
|
||||
.. warning:: By default, using `ufw` in conjunction will not have any effect. Please make sure to either bind the exposed
|
||||
ports of your docker container explicitly to 127.0.0.1 or configure docker to respect any set up firewall
|
||||
rules.
|
||||
|
||||
On this guide
|
||||
-------------
|
||||
@@ -57,8 +58,16 @@ directory writable to the user that runs pretix inside the docker container::
|
||||
Database
|
||||
--------
|
||||
|
||||
.. warning:: **Please use PostgreSQL for all new installations**. If you need to go for MySQL, make sure you run
|
||||
**MySQL 5.7 or newer** or **MariaDB 10.2.7 or newer**.
|
||||
|
||||
Next, we need a database and a database user. We can create these with any kind of database managing tool or directly on
|
||||
our database's shell. For PostgreSQL, we would do::
|
||||
our database's shell. Please make sure that UTF8 is used as encoding for the best compatibility. You can check this with
|
||||
the following command::
|
||||
|
||||
# sudo -u postgres psql -c 'SHOW SERVER_ENCODING'
|
||||
|
||||
For PostgreSQL database creation, we would do::
|
||||
|
||||
# sudo -u postgres createuser -P pretix
|
||||
# sudo -u postgres createdb -O pretix pretix
|
||||
@@ -82,6 +91,8 @@ When using MySQL, make sure you set the character set of the database to ``utf8m
|
||||
|
||||
mysql > CREATE DATABASE pretix DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
You will also need to make sure that ``sql_mode`` in your ``my.cnf`` file does **not** include ``ONLY_FULL_GROUP_BY``.
|
||||
|
||||
Redis
|
||||
-----
|
||||
|
||||
@@ -97,6 +108,18 @@ Now restart redis-server::
|
||||
|
||||
# systemctl restart redis-server
|
||||
|
||||
In this setup, systemd will delete ``/var/run/redis`` on every redis restart, which will cause issues with pretix. To
|
||||
prevent this, you can execute::
|
||||
|
||||
# systemctl edit redis-server
|
||||
|
||||
And insert the following::
|
||||
|
||||
[Service]
|
||||
# Keep the directory around so that pretix.service in docker does not need to be
|
||||
# restarted when redis is restarted.
|
||||
RuntimeDirectoryPreserve=yes
|
||||
|
||||
.. warning:: Setting the socket permissions to 777 is a possible security problem. If you have untrusted users on your
|
||||
system or have high security requirements, please don't do this and let redis listen to a TCP socket
|
||||
instead. We recommend the socket approach because the TCP socket in combination with docker's networking
|
||||
@@ -178,7 +201,7 @@ named ``/etc/systemd/system/pretix.service`` with the following content::
|
||||
TimeoutStartSec=0
|
||||
ExecStartPre=-/usr/bin/docker kill %n
|
||||
ExecStartPre=-/usr/bin/docker rm %n
|
||||
ExecStart=/usr/bin/docker run --name %n -p 8345:80 \
|
||||
ExecStart=/usr/bin/docker run --name %n -p 127.0.0.1:8345:80 \
|
||||
-v /var/pretix-data:/data \
|
||||
-v /etc/pretix:/etc/pretix \
|
||||
-v /var/run/redis:/var/run/redis \
|
||||
@@ -228,7 +251,7 @@ The following snippet is an example on how to configure a nginx proxy for pretix
|
||||
ssl_certificate_key /path/to/key.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:8345/;
|
||||
proxy_pass http://localhost:8345;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header Host $http_host;
|
||||
@@ -247,6 +270,8 @@ create an event and start selling tickets!
|
||||
|
||||
You should probably read :ref:`maintainance` next.
|
||||
|
||||
.. _`docker_updates`:
|
||||
|
||||
Updates
|
||||
-------
|
||||
|
||||
@@ -262,6 +287,8 @@ Restarting the service can take a few seconds, especially if the update requires
|
||||
Replace ``stable`` above with a specific version number like ``1.0`` or with ``latest`` for the development
|
||||
version, if you want to.
|
||||
|
||||
Make sure to also read :ref:`update_notes` and the release notes of the version you are updating to.
|
||||
|
||||
.. _`docker_plugininstall`:
|
||||
|
||||
Install a plugin
|
||||
|
||||
@@ -25,7 +25,7 @@ installation guides):
|
||||
* A HTTP reverse proxy, e.g. `nginx`_ or Apache to allow HTTPS connections
|
||||
* A `PostgreSQL`_ 9.6+, `MySQL`_ 5.7+, or MariaDB 10.2.7+ database server
|
||||
* A `redis`_ server
|
||||
* A `nodejs_` installation
|
||||
* A `nodejs`_ installation
|
||||
|
||||
We also recommend that you use a firewall, although this is not a pretix-specific recommendation. If you're new to
|
||||
Linux and firewalls, we recommend that you start with `ufw`_.
|
||||
@@ -34,9 +34,6 @@ Linux and firewalls, we recommend that you start with `ufw`_.
|
||||
SSL certificates can be obtained for free these days. We also *do not* provide support for HTTP-only
|
||||
installations except for evaluation purposes.
|
||||
|
||||
.. warning:: We recommend **PostgreSQL**. If you go for MySQL, make sure you run **MySQL 5.7 or newer** or
|
||||
**MariaDB 10.2.7 or newer**.
|
||||
|
||||
Unix user
|
||||
---------
|
||||
|
||||
@@ -50,8 +47,16 @@ In this guide, all code lines prepended with a ``#`` symbol are commands that yo
|
||||
Database
|
||||
--------
|
||||
|
||||
.. warning:: **Please use PostgreSQL for all new installations**. If you need to go for MySQL, make sure you run
|
||||
**MySQL 5.7 or newer** or **MariaDB 10.2.7 or newer**.
|
||||
|
||||
Having the database server installed, we still need a database and a database user. We can create these with any kind
|
||||
of database managing tool or directly on our database's shell. For PostgreSQL, we would do::
|
||||
of database managing tool or directly on our database's shell. Please make sure that UTF8 is used as encoding for the
|
||||
best compatibility. You can check this with the following command::
|
||||
|
||||
# sudo -u postgres psql -c 'SHOW SERVER_ENCODING'
|
||||
|
||||
For PostgreSQL database creation, we would do::
|
||||
|
||||
# sudo -u postgres createuser pretix
|
||||
# sudo -u postgres createdb -O pretix pretix
|
||||
@@ -60,6 +65,8 @@ When using MySQL, make sure you set the character set of the database to ``utf8m
|
||||
|
||||
mysql > CREATE DATABASE pretix DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
You will also need to make sure that ``sql_mode`` in your ``my.cnf`` file does **not** include ``ONLY_FULL_GROUP_BY``.
|
||||
|
||||
Package dependencies
|
||||
--------------------
|
||||
|
||||
@@ -67,7 +74,7 @@ To build and run pretix, you will need the following debian packages::
|
||||
|
||||
# apt-get install git build-essential python-dev python3-venv python3 python3-pip \
|
||||
python3-dev libxml2-dev libxslt1-dev libffi-dev zlib1g-dev libssl-dev \
|
||||
gettext libpq-dev libmariadbclient-dev libjpeg-dev libopenjp2-7-dev
|
||||
gettext libpq-dev libmariadb-dev libjpeg-dev libopenjp2-7-dev
|
||||
|
||||
Config file
|
||||
-----------
|
||||
@@ -129,12 +136,15 @@ python installation::
|
||||
$ source /var/pretix/venv/bin/activate
|
||||
(venv)$ pip3 install -U pip setuptools wheel
|
||||
|
||||
We now install pretix, its direct dependencies and gunicorn. Replace ``postgres`` with ``mysql`` in the following
|
||||
command if you're running MySQL::
|
||||
We now install pretix, its direct dependencies and gunicorn::
|
||||
|
||||
(venv)$ pip3 install "pretix[postgres]" gunicorn
|
||||
(venv)$ pip3 install pretix gunicorn
|
||||
|
||||
Note that you need Python 3.6 or newer. You can find out your Python version using ``python -V``.
|
||||
If you're running MySQL, also install the client library::
|
||||
|
||||
(venv)$ pip3 install mysqlclient
|
||||
|
||||
Note that you need Python 3.7 or newer. You can find out your Python version using ``python -V``.
|
||||
|
||||
We also need to create a data directory::
|
||||
|
||||
@@ -229,7 +239,7 @@ The following snippet is an example on how to configure a nginx proxy for pretix
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:8345/;
|
||||
proxy_pass http://localhost:8345;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header Host $http_host;
|
||||
@@ -251,14 +261,14 @@ The following snippet is an example on how to configure a nginx proxy for pretix
|
||||
}
|
||||
|
||||
location /static/ {
|
||||
alias /var/pretix/venv/lib/python3.5/site-packages/pretix/static.dist/;
|
||||
alias /var/pretix/venv/lib/python3.10/site-packages/pretix/static.dist/;
|
||||
access_log off;
|
||||
expires 365d;
|
||||
add_header Cache-Control "public";
|
||||
}
|
||||
}
|
||||
|
||||
.. note:: Remember to replace the ``python3.5`` in the ``/static/`` path in the config
|
||||
.. note:: Remember to replace the ``python3.10`` in the ``/static/`` path in the config
|
||||
above with your python version.
|
||||
|
||||
We recommend reading about setting `strong encryption settings`_ for your web server.
|
||||
@@ -272,21 +282,23 @@ create an event and start selling tickets!
|
||||
|
||||
You should probably read :ref:`maintainance` next.
|
||||
|
||||
.. _`manual_updates`:
|
||||
|
||||
Updates
|
||||
-------
|
||||
|
||||
.. warning:: While we try hard not to break things, **please perform a backup before every upgrade**.
|
||||
|
||||
To upgrade to a new pretix release, pull the latest code changes and run the following commands (again, replace
|
||||
``postgres`` with ``mysql`` if necessary)::
|
||||
To upgrade to a new pretix release, pull the latest code changes and run the following commands::
|
||||
|
||||
$ source /var/pretix/venv/bin/activate
|
||||
(venv)$ pip3 install -U pretix[postgres] gunicorn
|
||||
(venv)$ pip3 install -U --upgrade-strategy eager pretix gunicorn
|
||||
(venv)$ python -m pretix migrate
|
||||
(venv)$ python -m pretix rebuild
|
||||
(venv)$ python -m pretix updatestyles
|
||||
# systemctl restart pretix-web pretix-worker
|
||||
|
||||
Make sure to also read :ref:`update_notes` and the release notes of the version you are updating to.
|
||||
|
||||
.. _`manual_plugininstall`:
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ If you host your own pretix instance, you also need to care about the availabili
|
||||
of your service and the safety of your data yourself. This page gives you some
|
||||
information that you might need to do so properly.
|
||||
|
||||
.. _`backups`:
|
||||
|
||||
Backups
|
||||
-------
|
||||
|
||||
|
||||
51
doc/admin/updates.rst
Normal file
51
doc/admin/updates.rst
Normal file
@@ -0,0 +1,51 @@
|
||||
.. _`update_notes`:
|
||||
|
||||
Update notes
|
||||
============
|
||||
|
||||
pretix receives regular feature and bugfix updates and we highly encourage you to always update to
|
||||
the latest version for maximum quality and security. Updates are announces on our `blog`_. There are
|
||||
usually 10 feature updates in a year, so you can expect a new release almost every month.
|
||||
|
||||
Pure bugfix releases are only issued in case of very critical bugs or security vulnerabilities. In these
|
||||
case, we'll publish bugfix releases for the last three stable release branches.
|
||||
|
||||
Compatibility to plugins and in very rare cases API clients may break. For in-depth details on the
|
||||
API changes of every version, please refer to the release notes published on our blog.
|
||||
|
||||
Upgrade steps
|
||||
-------------
|
||||
|
||||
For the actual upgrade, you can usually just follow the steps from the installation guide for :ref:`manual installations <manual_updates>`
|
||||
or :ref:`docker installations <docker_updates>` respectively.
|
||||
Generally, it is always strongly recommended to perform a :ref:`backup <backups>` first.
|
||||
It is possible to skip versions during updates, although we recommend not skipping over major version numbers
|
||||
(i.e. if you want to go from 2.4 to 4.4, first upgrade to 3.0, then upgrade to 4.0, then to 4.4).
|
||||
|
||||
In addition to these standard update steps, the following list issues steps that should be taken when you upgrade
|
||||
to specific versions for pretix. If you're skipping versions, please read the instructions for every version in
|
||||
between as well.
|
||||
|
||||
Upgrade to 3.17.0 or newer
|
||||
""""""""""""""""""""""""""
|
||||
|
||||
pretix 3.17 introduces a dependency on ``nodejs``, so you should install it on your system::
|
||||
|
||||
# apt install nodejs npm
|
||||
|
||||
Upgrade to 4.4.0 or newer
|
||||
"""""""""""""""""""""""""
|
||||
|
||||
pretix 4.4 introduces a new data structure to store historical financial data. If you already have existing
|
||||
data in your database, you will need to back-fill this data or you might get incorrect reports! This is not
|
||||
done automatically as part of the usual update steps since it can take a while on large databases and you might
|
||||
want to do it in parallel while the system is already running again. Please execute the following command::
|
||||
|
||||
(venv)$ python -m pretix create_order_transactions
|
||||
|
||||
Or, with a docker installation::
|
||||
|
||||
$ docker exec -it pretix.service pretix create_order_transactions
|
||||
|
||||
|
||||
.. _blog: https://pretix.eu/about/en/blog/
|
||||
@@ -99,7 +99,8 @@ following endpoint:
|
||||
"hardware_brand": "Samsung",
|
||||
"hardware_model": "Galaxy S",
|
||||
"software_brand": "pretixdroid",
|
||||
"software_version": "4.1.0"
|
||||
"software_version": "4.1.0",
|
||||
"info": {"arbitrary": "data"}
|
||||
}
|
||||
|
||||
You will receive a response equivalent to the response of your initialization request.
|
||||
|
||||
@@ -43,6 +43,8 @@ Possible permissions are:
|
||||
* Can view vouchers
|
||||
* Can change vouchers
|
||||
|
||||
.. _`rest-compat`:
|
||||
|
||||
Compatibility
|
||||
-------------
|
||||
|
||||
@@ -87,7 +89,8 @@ respectively, or ``null`` if there is no such page. You can use those URLs to re
|
||||
respective page.
|
||||
|
||||
The field ``results`` contains a list of objects representing the first results. For most
|
||||
objects, every page contains 50 results.
|
||||
objects, every page contains 50 results. You can specify a lower pagination size using the
|
||||
``page_size`` query parameter, but no more than 50.
|
||||
|
||||
Conditional fetching
|
||||
--------------------
|
||||
|
||||
@@ -97,7 +97,8 @@ For example, if you want users to be redirected to ``https://example.org/order/r
|
||||
either enter ``https://example.org`` or ``https://example.org/order/``.
|
||||
|
||||
The user will be redirected back to your page instead of pretix' order confirmation page after the payment,
|
||||
**regardless of whether it was successful or not**. Make sure you use our API to check if the payment actually
|
||||
**regardless of whether it was successful or not**. We will append an ``error=…`` query parameter with an error
|
||||
message, but you should not rely on that and instead make sure you use our API to check if the payment actually
|
||||
worked! Your final URL could look like this::
|
||||
|
||||
https://test.pretix.eu/democon/3vjrh/order/NSLEZ/ujbrnsjzbq4dzhck/pay/123/?return_url=https%3A%2F%2Fexample.org%2Forder%2Freturn%3Ftx_id%3D1234
|
||||
|
||||
@@ -243,6 +243,99 @@ Cart position endpoints
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this
|
||||
order.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/cartpositions/bulk_create/
|
||||
|
||||
Creates multiple new cart position. This operation is deliberately not atomic, so each cart position can succeed
|
||||
or fail individually, so the response code of the response is not the only thing to look at!
|
||||
|
||||
.. warning:: This endpoint is considered **experimental**. It might change at any time without prior notice.
|
||||
|
||||
.. warning:: The same limitations as with the regular creation endpoint apply.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/cartpositions/bulk_create/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
[
|
||||
{
|
||||
"item": 1,
|
||||
"variation": null,
|
||||
"price": "23.00",
|
||||
"attendee_name_parts": {
|
||||
"given_name": "Peter",
|
||||
"family_name": "Miller"
|
||||
},
|
||||
"attendee_email": null,
|
||||
"answers": [
|
||||
{
|
||||
"question": 1,
|
||||
"answer": "23",
|
||||
"options": []
|
||||
}
|
||||
],
|
||||
"subevent": null
|
||||
},
|
||||
{
|
||||
"item": 1,
|
||||
"variation": null,
|
||||
"price": "23.00",
|
||||
"attendee_name_parts": {
|
||||
"given_name": "Maria",
|
||||
"family_name": "Miller"
|
||||
},
|
||||
"attendee_email": null,
|
||||
"answers": [
|
||||
{
|
||||
"question": 1,
|
||||
"answer": "23",
|
||||
"options": []
|
||||
}
|
||||
],
|
||||
"subevent": null
|
||||
}
|
||||
]
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"success": true,
|
||||
"errors": null,
|
||||
"data": {
|
||||
"id": 1,
|
||||
...
|
||||
},
|
||||
},
|
||||
{
|
||||
"success": "false",
|
||||
"errors": {
|
||||
"non_field_errors": ["There is not enough quota available on quota \"Tickets\" to perform the operation."]
|
||||
},
|
||||
"data": null
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer of the event to create positions for
|
||||
:param event: The ``slug`` field of the event to create positions for
|
||||
:statuscode 200: See response for success
|
||||
:statuscode 400: Your input could not be parsed
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this
|
||||
order.
|
||||
|
||||
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/cartpositions/(id)/
|
||||
|
||||
Deletes a cart position, identified by its internal ID.
|
||||
|
||||
@@ -362,6 +362,42 @@ Endpoints
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/(list)/failed_checkins/
|
||||
|
||||
Stores a failed check-in. Only necessary for statistical purposes if you perform scan validation offline.
|
||||
|
||||
:<json boolean error_reason: One of ``canceled``, ``invalid``, ``unpaid``, ``product``, ``rules``, ``revoked``,
|
||||
``incomplete``, ``already_redeemed``, or ``error``. Required.
|
||||
:<json raw_barcode: The raw barcode you scanned. Required.
|
||||
:<json datetime: Date and time of the scan. Optional.
|
||||
:<json type: Type of scan, defaults to ``"entry"``.
|
||||
:<json position: Internal ID of an order position you matched. Optional.
|
||||
:<json raw_item: Internal ID of an item you matched. Optional.
|
||||
:<json raw_variation: Internal ID of an item variation you matched. Optional.
|
||||
:<json raw_subevent: Internal ID of an event series date you matched. Optional.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/checkinlists/1/failed_checkins/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"raw_barcode": "Pvrk50vUzQd0DhdpNRL4I4OcXsvg70uA",
|
||||
"error_reason": "canceled"
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:param list: The ID of the check-in list to save for
|
||||
:statuscode 201: no error
|
||||
:statuscode 400: Invalid request
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
:statuscode 404: The requested order position or check-in list does not exist.
|
||||
|
||||
|
||||
Order position endpoints
|
||||
------------------------
|
||||
@@ -424,6 +460,9 @@ Order position endpoints
|
||||
"checkins": [
|
||||
{
|
||||
"list": 1,
|
||||
"type": "entry",
|
||||
"gate": null,
|
||||
"device": 2,
|
||||
"datetime": "2017-12-25T12:45:23Z",
|
||||
"auto_checked_in": true
|
||||
}
|
||||
@@ -535,6 +574,9 @@ Order position endpoints
|
||||
{
|
||||
"list": 1,
|
||||
"datetime": "2017-12-25T12:45:23Z",
|
||||
"type": "entry",
|
||||
"gate": null,
|
||||
"device": 2,
|
||||
"auto_checked_in": true
|
||||
}
|
||||
],
|
||||
@@ -562,6 +604,8 @@ Order position endpoints
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
:statuscode 404: The requested order position or check-in list does not exist.
|
||||
|
||||
.. _`rest-checkin-redeem`:
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/(list)/positions/(id)/redeem/
|
||||
|
||||
Tries to redeem an order position, identified by its internal ID, i.e. checks the attendee in. This endpoint
|
||||
@@ -576,8 +620,9 @@ Order position endpoints
|
||||
:<json boolean canceled_supported: When this parameter is set to ``true``, the response code ``canceled`` may be
|
||||
returned. Otherwise, canceled orders will return ``unpaid``.
|
||||
:<json datetime datetime: Specifies the datetime of the check-in. If not supplied, the current time will be used.
|
||||
:<json boolean force: Specifies that the check-in should succeed regardless of previous check-ins or required
|
||||
questions that have not been filled. Defaults to ``false``.
|
||||
:<json boolean force: Specifies that the check-in should succeed regardless of revoked barcode, previous check-ins or required
|
||||
questions that have not been filled. This is usually used to upload offline scans that already happened,
|
||||
because there's no point in validating them since they happened whether they are valid or not. Defaults to ``false``.
|
||||
:<json string type: Send ``"exit"`` for an exit and ``"entry"`` (default) for an entry.
|
||||
:<json boolean ignore_unpaid: Specifies that the check-in should succeed even if the order is in pending state.
|
||||
Defaults to ``false`` and only works when ``include_pending`` is set on the check-in
|
||||
|
||||
@@ -84,6 +84,12 @@ Endpoints
|
||||
|
||||
The ``clone_from`` parameter has been added to the event creation endpoint.
|
||||
|
||||
.. versionchanged:: 4.1
|
||||
|
||||
The ``with_availability_for`` parameter has been added.
|
||||
|
||||
The ``search`` query parameter has been added to filter events by their slug, name, or location in any language.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/
|
||||
|
||||
Returns a list of all events within a given organizer the authenticated user/token has access to.
|
||||
@@ -162,6 +168,11 @@ Endpoints
|
||||
events having set their ``Format`` meta data to ``Seminar``, ``?attr[Format]=`` only those, that have no value
|
||||
set. Please note that this filter will respect default values set on organizer level.
|
||||
:query sales_channel: If set to a sales channel identifier, only events allowed to be sold on the specified sales channel are returned.
|
||||
:query with_availability_for: If set to a sales channel identifier, the response will contain a special ``best_availability_state``
|
||||
attribute with values of 100 for "tickets available", values less than 100 for "tickets sold out or reserved",
|
||||
and ``null`` for "status unknown". These values might be served from a cache. This parameter can make the response
|
||||
slow.
|
||||
:query search: Only return events matching a given search query.
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
Resources and endpoints
|
||||
=======================
|
||||
|
||||
With a few exceptions, this only lists resources bundled in the pretix core modules.
|
||||
Additional endpoints are provided by pretix plugins. Some of them are documented
|
||||
at :ref:`plugin-docs`.
|
||||
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
@@ -31,5 +36,6 @@ Resources and endpoints
|
||||
webhooks
|
||||
seatingplans
|
||||
exporters
|
||||
sendmail_rules
|
||||
billing_invoices
|
||||
billing_var
|
||||
billing_var
|
||||
@@ -58,6 +58,21 @@ lines list of objects The actual invo
|
||||
created before this field was introduced as well as for
|
||||
all lines not created by a product (e.g. a shipping or
|
||||
cancellation fee).
|
||||
├ subevent integer Event series date ID used to create this line. Note that everything
|
||||
about the subevent might have changed since the creation
|
||||
of the invoice. Can be ``null`` for all invoice lines
|
||||
created before this field was introduced as well as for
|
||||
all lines not created by a product (e.g. a shipping or
|
||||
cancellation fee) as well as for all events that are not a series.
|
||||
├ fee_type string Fee type, e.g. ``shipping``, ``service``, ``payment``,
|
||||
``cancellation``, ``giftcard``, or ``other. Can be ``null`` for
|
||||
all invoice lines
|
||||
created before this field was introduced as well as for
|
||||
all lines not created by a fee (e.g. a product).
|
||||
├ fee_internal_type string Additional fee type, e.g. type of payment provider. Can be ``null``
|
||||
for all invoice lines
|
||||
created before this field was introduced as well as for
|
||||
all lines not created by a fee (e.g. a product).
|
||||
├ event_date_from datetime Start date of the (sub)event this line was created for as it
|
||||
was set during invoice creation. Can be ``null`` for all invoice
|
||||
lines created before this was introduced as well as for lines in
|
||||
@@ -69,6 +84,12 @@ lines list of objects The actual invo
|
||||
an event series not created by a product (e.g. shipping or
|
||||
cancellation fees) as well as whenever the respective (sub)event
|
||||
has no end date set.
|
||||
├ event_location string Location of the (sub)event this line was created for as it
|
||||
was set during invoice creation. Can be ``null`` for all invoice
|
||||
lines created before this was introduced as well as for lines in
|
||||
an event series not created by a product (e.g. shipping or
|
||||
cancellation fees) as well as whenever the respective (sub)event
|
||||
has no location set.
|
||||
├ attendee_name string Attendee name at time of invoice creation. Can be ``null`` if no
|
||||
name was set or if names are configured to not be added to invoices.
|
||||
├ gross_value money (string) Price including taxes
|
||||
@@ -97,6 +118,18 @@ internal_reference string Customer's refe
|
||||
``lines.event_date_to``, and ``lines.attendee_name`` have been added.
|
||||
``refers`` now returns an invoice number including the prefix.
|
||||
|
||||
.. versionchanged:: 4.1
|
||||
|
||||
The attributes ``fee_type`` and ``fee_internal_type`` have been added.
|
||||
|
||||
.. versionchanged:: 4.1
|
||||
|
||||
The attribute ``lines.event_location`` has been added.
|
||||
|
||||
.. versionchanged:: 4.6
|
||||
|
||||
The attribute ``lines.subevent`` has been added.
|
||||
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
@@ -162,8 +195,12 @@ Endpoints
|
||||
"description": "Budget Ticket",
|
||||
"item": 1234,
|
||||
"variation": 245,
|
||||
"subevent": null,
|
||||
"fee_type": null,
|
||||
"fee_internal_type": null,
|
||||
"event_date_from": "2017-12-27T10:00:00Z",
|
||||
"event_date_to": null,
|
||||
"event_location": "Heidelberg",
|
||||
"attendee_name": null,
|
||||
"gross_value": "23.00",
|
||||
"tax_value": "0.00",
|
||||
@@ -248,8 +285,12 @@ Endpoints
|
||||
"description": "Budget Ticket",
|
||||
"item": 1234,
|
||||
"variation": 245,
|
||||
"subevent": null,
|
||||
"fee_type": null,
|
||||
"fee_internal_type": null,
|
||||
"event_date_from": "2017-12-27T10:00:00Z",
|
||||
"event_date_to": null,
|
||||
"event_location": "Heidelberg",
|
||||
"attendee_name": null,
|
||||
"gross_value": "23.00",
|
||||
"tax_value": "0.00",
|
||||
|
||||
@@ -24,8 +24,25 @@ active boolean If ``false``, t
|
||||
description multi-lingual string A public description of the variation. May contain
|
||||
Markdown syntax or can be ``null``.
|
||||
position integer An integer, used for sorting
|
||||
require_approval boolean If ``true``, orders with this variation will need to be
|
||||
approved by the event organizer before they can be
|
||||
paid.
|
||||
require_membership boolean If ``true``, booking this variation requires an active membership.
|
||||
require_membership_hidden boolean If ``true`` and ``require_membership`` is set, this variation will
|
||||
be hidden from users without a valid membership.
|
||||
require_membership_types list of integers Internal IDs of membership types valid if ``require_membership`` is ``true``
|
||||
sales_channels list of strings Sales channels this variation is available on, such as
|
||||
``"web"`` or ``"resellers"``. Defaults to all existing sales channels.
|
||||
The item-level list takes precedence, i.e. a sales
|
||||
channel needs to be on both lists for the item to be
|
||||
available.
|
||||
available_from datetime The first date time at which this variation can be bought
|
||||
(or ``null``).
|
||||
available_until datetime The last date time at which this variation can be bought
|
||||
(or ``null``).
|
||||
hide_without_voucher boolean If ``true``, this variation is only shown during the voucher
|
||||
redemption process, but not in the normal shop
|
||||
frontend.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
Endpoints
|
||||
@@ -62,8 +79,14 @@ Endpoints
|
||||
"en": "S"
|
||||
},
|
||||
"active": true,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"hide_without_voucher": false,
|
||||
"description": {
|
||||
"en": "Test2"
|
||||
},
|
||||
@@ -78,7 +101,9 @@ Endpoints
|
||||
"en": "L"
|
||||
},
|
||||
"active": true,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
"description": {},
|
||||
"position": 1,
|
||||
@@ -127,8 +152,14 @@ Endpoints
|
||||
"price": "10.00",
|
||||
"original_price": null,
|
||||
"active": true,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"position": 0
|
||||
}
|
||||
@@ -158,8 +189,14 @@ Endpoints
|
||||
"value": {"en": "Student"},
|
||||
"default_price": "10.00",
|
||||
"active": true,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"position": 0
|
||||
}
|
||||
@@ -179,8 +216,14 @@ Endpoints
|
||||
"price": "10.00",
|
||||
"original_price": null,
|
||||
"active": true,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"position": 0
|
||||
}
|
||||
@@ -231,8 +274,14 @@ Endpoints
|
||||
"price": "10.00",
|
||||
"original_price": null,
|
||||
"active": false,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"position": 1
|
||||
}
|
||||
|
||||
@@ -70,6 +70,8 @@ require_approval boolean If ``true``, or
|
||||
paid.
|
||||
require_bundling boolean If ``true``, this item is only available as part of bundles.
|
||||
require_membership boolean If ``true``, booking this item requires an active membership.
|
||||
require_membership_hidden boolean If ``true`` and ``require_membership`` is set, this product will
|
||||
be hidden from users without a valid membership.
|
||||
require_membership_types list of integers Internal IDs of membership types valid if ``require_membership`` is ``true``
|
||||
grant_membership_type integer If set to the internal ID of a membership type, purchasing this item will
|
||||
create a membership of the given type.
|
||||
@@ -105,8 +107,22 @@ variations list of objects A list with one
|
||||
├ active boolean If ``false``, this variation will not be sold or shown.
|
||||
├ description multi-lingual string A public description of the variation. May contain
|
||||
├ require_membership boolean If ``true``, booking this variation requires an active membership.
|
||||
├ require_membership_hidden boolean If ``true`` and ``require_membership`` is set, this variation will
|
||||
be hidden from users without a valid membership.
|
||||
├ require_membership_types list of integers Internal IDs of membership types valid if ``require_membership`` is ``true``
|
||||
Markdown syntax or can be ``null``.
|
||||
├ sales_channels list of strings Sales channels this variation is available on, such as
|
||||
``"web"`` or ``"resellers"``. Defaults to all existing sales channels.
|
||||
The item-level list takes precedence, i.e. a sales
|
||||
channel needs to be on both lists for the item to be
|
||||
available.
|
||||
├ available_from datetime The first date time at which this variation can be bought
|
||||
(or ``null``).
|
||||
├ available_until datetime The last date time at which this variation can be bought
|
||||
(or ``null``).
|
||||
├ hide_without_voucher boolean If ``true``, this variation is only shown during the voucher
|
||||
redemption process, but not in the normal shop
|
||||
frontend.
|
||||
└ position integer An integer, used for sorting
|
||||
addons list of objects Definition of add-ons that can be chosen for this item.
|
||||
Only writable during creation,
|
||||
@@ -143,6 +159,10 @@ meta_data object Values set for
|
||||
The attributes ``require_membership``, ``require_membership_types``, ``grant_membership_type``, ``grant_membership_duration_like_event``,
|
||||
``grant_membership_duration_days`` and ``grant_membership_duration_months`` have been added.
|
||||
|
||||
.. versionchanged:: 4.4
|
||||
|
||||
The attributes ``require_membership_hidden`` attribute has been added.
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
@@ -230,6 +250,10 @@ Endpoints
|
||||
"active": true,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"position": 0
|
||||
},
|
||||
@@ -241,6 +265,10 @@ Endpoints
|
||||
"active": true,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"position": 1
|
||||
}
|
||||
@@ -337,6 +365,10 @@ Endpoints
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"description": null,
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"hide_without_voucher": false,
|
||||
"position": 0
|
||||
},
|
||||
{
|
||||
@@ -347,6 +379,10 @@ Endpoints
|
||||
"active": true,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"position": 1
|
||||
}
|
||||
@@ -422,6 +458,10 @@ Endpoints
|
||||
"active": true,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"position": 0
|
||||
},
|
||||
@@ -433,6 +473,10 @@ Endpoints
|
||||
"active": true,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"position": 1
|
||||
}
|
||||
@@ -497,6 +541,10 @@ Endpoints
|
||||
"active": true,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"position": 0
|
||||
},
|
||||
@@ -508,6 +556,10 @@ Endpoints
|
||||
"active": true,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"position": 1
|
||||
}
|
||||
@@ -603,6 +655,10 @@ Endpoints
|
||||
"active": true,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"position": 0
|
||||
},
|
||||
@@ -614,6 +670,10 @@ Endpoints
|
||||
"active": true,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"position": 1
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ payment_date date **DEPRECATED AN
|
||||
payment_provider string **DEPRECATED AND INACCURATE** Payment provider used for this order
|
||||
total money (string) Total value of this order
|
||||
comment string Internal comment on this order
|
||||
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
|
||||
that this ticket requires special attention if a ticket
|
||||
of this order is scanned.
|
||||
@@ -123,6 +124,18 @@ last_modified datetime Last modificati
|
||||
|
||||
The ``customer`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 4.1
|
||||
|
||||
The ``custom_followup_at`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 4.4
|
||||
|
||||
The ``item`` and ``variation`` query parameters have been added.
|
||||
|
||||
.. versionchanged:: 4.6
|
||||
|
||||
The ``subevent`` query parameters has been added.
|
||||
|
||||
|
||||
.. _order-position-resource:
|
||||
|
||||
@@ -160,11 +173,13 @@ secret string Secret code pri
|
||||
addon_to integer Internal ID of the position this position is an add-on for (or ``null``)
|
||||
subevent integer ID of the date inside an event series this position belongs to (or ``null``).
|
||||
pseudonymization_id string A random ID, e.g. for use in lead scanning apps
|
||||
checkins list of objects List of check-ins with this ticket
|
||||
checkins list of objects List of **successful** check-ins with this ticket
|
||||
├ id integer Internal ID of the check-in event
|
||||
├ list integer Internal ID of the check-in list
|
||||
├ datetime datetime Time of check-in
|
||||
├ type string Type of scan (defaults to ``entry``)
|
||||
├ gate integer Internal ID of the gate. Can be ``null``.
|
||||
├ device integer Internal ID of the device. Can be ``null``.
|
||||
└ auto_checked_in boolean Indicates if this check-in been performed automatically by the system
|
||||
downloads list of objects List of ticket download options
|
||||
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
|
||||
@@ -305,6 +320,7 @@ List of all orders
|
||||
"fees": [],
|
||||
"total": "23.00",
|
||||
"comment": "",
|
||||
"custom_followup_at": null,
|
||||
"checkin_attention": false,
|
||||
"require_approval": false,
|
||||
"invoice_address": {
|
||||
@@ -355,6 +371,8 @@ List of all orders
|
||||
{
|
||||
"list": 44,
|
||||
"type": "entry",
|
||||
"gate": null,
|
||||
"device": 2,
|
||||
"datetime": "2017-12-25T12:45:23Z",
|
||||
"auto_checked_in": false
|
||||
}
|
||||
@@ -405,6 +423,8 @@ List of all orders
|
||||
:query string code: Only return orders that match the given order code
|
||||
:query string status: Only return orders in the given order status (see above)
|
||||
:query string search: Only return orders matching a given search query
|
||||
:query integer item: Only return orders with a position that contains this item ID. *Warning:* Result will also include orders if they contain mixed items, and it will even return orders where the item is only contained in a canceled position.
|
||||
:query integer variation: Only return orders with a position that contains this variation ID. *Warning:* Result will also include orders if they contain mixed items and variations, and it will even return orders where the variation is only contained in a canceled position.
|
||||
:query boolean testmode: Only return orders with ``testmode`` set to ``true`` or ``false``
|
||||
:query boolean require_approval: If set to ``true`` or ``false``, only categories with this value for the field
|
||||
``require_approval`` will be returned.
|
||||
@@ -417,6 +437,7 @@ List of all orders
|
||||
recommend using this in combination with ``testmode=false``, since test mode orders can vanish at any time and
|
||||
you will not notice it using this method.
|
||||
:query datetime created_since: Only return orders that have been created since the given date.
|
||||
:query integer subevent: Only return orders with a position that contains this subevent ID. *Warning:* Result will also include orders if they contain mixed subevents, and it will even return orders where the subevent is only contained in a canceled position.
|
||||
:query datetime subevent_after: Only return orders that contain a ticket for a subevent taking place after the given date. This is an exclusive after, and it considers the **end** of the subevent (or its start, if the end is not set).
|
||||
:query datetime subevent_before: Only return orders that contain a ticket for a subevent taking place after the given date. This is an exclusive before, and it considers the **start** of the subevent.
|
||||
:query string 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.
|
||||
@@ -474,6 +495,7 @@ Fetching individual orders
|
||||
"fees": [],
|
||||
"total": "23.00",
|
||||
"comment": "",
|
||||
"custom_followup_at": null,
|
||||
"checkin_attention": false,
|
||||
"require_approval": false,
|
||||
"invoice_address": {
|
||||
@@ -524,6 +546,8 @@ Fetching individual orders
|
||||
{
|
||||
"list": 44,
|
||||
"type": "entry",
|
||||
"gate": null,
|
||||
"device": 2,
|
||||
"datetime": "2017-12-25T12:45:23Z",
|
||||
"auto_checked_in": false
|
||||
}
|
||||
@@ -638,6 +662,8 @@ Updating order fields
|
||||
|
||||
* ``comment``
|
||||
|
||||
* ``custom_followup_at``
|
||||
|
||||
* ``invoice_address`` (you always need to supply the full object, or ``null`` to delete the current address)
|
||||
|
||||
**Example request**:
|
||||
@@ -811,7 +837,9 @@ Creating orders
|
||||
charge will be created), this is just informative in case you *handled the payment already*.
|
||||
* ``payment_date`` (optional) – Date and time of the completion of the payment.
|
||||
* ``comment`` (optional)
|
||||
* ``custom_followup_at`` (optional)
|
||||
* ``checkin_attention`` (optional)
|
||||
* ``require_approval`` (optional)
|
||||
* ``invoice_address`` (optional)
|
||||
|
||||
* ``company``
|
||||
@@ -871,8 +899,9 @@ Creating orders
|
||||
|
||||
* ``force`` (optional). If set to ``true``, quotas will be ignored.
|
||||
* ``send_email`` (optional). If set to ``true``, the same emails will be sent as for a regular order, regardless of
|
||||
whether these emails are enabled for certain sales channels. Defaults to
|
||||
``false``. Used to be ``send_mail`` before pretix 3.14.
|
||||
whether these emails are enabled for certain sales channels. If set to ``null``, behavior will be controlled by pretix'
|
||||
settings based on the sales channels (added in pretix 4.7). Defaults to ``false``.
|
||||
Used to be ``send_mail`` before pretix 3.14.
|
||||
|
||||
If you want to use add-on products, you need to set the ``positionid`` fields of all positions manually
|
||||
to incrementing integers starting with ``1``. Then, you can reference one of these
|
||||
@@ -1421,6 +1450,8 @@ List of all order positions
|
||||
{
|
||||
"list": 44,
|
||||
"type": "entry",
|
||||
"gate": null,
|
||||
"device": 2,
|
||||
"datetime": "2017-12-25T12:45:23Z",
|
||||
"auto_checked_in": false
|
||||
}
|
||||
@@ -1527,6 +1558,8 @@ Fetching individual positions
|
||||
{
|
||||
"list": 44,
|
||||
"type": "entry",
|
||||
"gate": null,
|
||||
"device": 2,
|
||||
"datetime": "2017-12-25T12:45:23Z",
|
||||
"auto_checked_in": false
|
||||
}
|
||||
|
||||
@@ -28,12 +28,22 @@ closed boolean Whether the quo
|
||||
field).
|
||||
release_after_exit boolean Whether the quota regains capacity as soon as some tickets
|
||||
have been scanned at an exit.
|
||||
available boolean Whether this quota is available. Only returned if ``with_availability=true``
|
||||
is set on the request. Do not rely on this value for critical operations, it may be
|
||||
slightly out of date.
|
||||
available_number integer Number of available tickets. Only returned if ``with_availability=true``
|
||||
is set on the request. Do not rely on this value for critical operations, it may be
|
||||
slightly out of date. ``null`` means unlimited.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 3.10
|
||||
|
||||
The attribute ``release_after_exit`` has been added.
|
||||
|
||||
.. versionchanged:: 4.1
|
||||
|
||||
The ``with_availability`` query parameter has been added.
|
||||
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
@@ -80,6 +90,7 @@ Endpoints
|
||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``id`` and ``position``.
|
||||
Default: ``position``
|
||||
:query integer subevent: Only return quotas of the sub-event with the given ID
|
||||
:query string with_availability: Set to ``true`` to get availability information. Can lead to increased answer times.
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:statuscode 200: no error
|
||||
@@ -120,6 +131,7 @@ Endpoints
|
||||
: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 quota to fetch
|
||||
:query string with_availability: Set to ``true`` to get availability information. Can lead to increased answer times.
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
|
||||
281
doc/api/resources/sendmail_rules.rst
Normal file
281
doc/api/resources/sendmail_rules.rst
Normal file
@@ -0,0 +1,281 @@
|
||||
Automated email rules
|
||||
=====================
|
||||
|
||||
Resource description
|
||||
--------------------
|
||||
|
||||
Automated email rules that specify emails that the system will send automatically at a specific point in time, e.g.
|
||||
the day of the event.
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
===================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal ID of the rule
|
||||
enabled boolean If ``false``, the rule is ignored
|
||||
subject multi-lingual string The subject of the email
|
||||
template multi-lingual string The body of the email
|
||||
all_products boolean If ``true``, the email is sent to buyers of all products
|
||||
limit_products list of integers List of product IDs, if ``all_products`` is not set
|
||||
include_pending boolean If ``true``, the email is sent to pending orders. If ``false``,
|
||||
only paid orders are considered.
|
||||
date_is_absolute boolean If ``true``, the email is set at a specific point in time.
|
||||
send_date datetime If ``date_is_absolute`` is set: Date and time to send the email.
|
||||
send_offset_days integer If ``date_is_absolute`` is not set, this is the number of days
|
||||
before/after the email is sent.
|
||||
send_offset_time time If ``date_is_absolute`` is not set, this is the time of day the
|
||||
email is sent on the day specified by ``send_offset_days``.
|
||||
offset_to_event_end boolean If ``true``, ``send_offset_days`` is relative to the event end
|
||||
date. Otherwise it is relative to the event start date.
|
||||
offset_is_after boolean If ``true``, ``send_offset_days`` is the number of days **after**
|
||||
the event start or end date. Otherwise it is the number of days
|
||||
**before**.
|
||||
send_to string Can be ``"orders"`` if the email should be sent to customers
|
||||
(one email per order),
|
||||
``"attendees"`` if the email should be sent to every attendee,
|
||||
or ``"both"``.
|
||||
date. Otherwise it is relative to the event start date.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/sendmail_rules/
|
||||
|
||||
Returns a list of all rules configured for an event.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/sendmail_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,
|
||||
"enabled": true,
|
||||
"subject": {"en": "See you tomorrow!"},
|
||||
"template": {"en": "Don't forget your tickets, download them at {url}"},
|
||||
"all_products": true,
|
||||
"limit_products": [],
|
||||
"include_pending": false,
|
||||
"send_date": null,
|
||||
"send_offset_days": 1,
|
||||
"send_offset_time": "18:00",
|
||||
"date_is_absolute": false,
|
||||
"offset_to_event_end": false,
|
||||
"offset_is_after": false,
|
||||
"send_to": "orders"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
: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)/sendmail_rules/(id)/
|
||||
|
||||
Returns information on one rule, identified by its ID.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/sendmail_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,
|
||||
"enabled": true,
|
||||
"subject": {"en": "See you tomorrow!"},
|
||||
"template": {"en": "Don't forget your tickets, download them at {url}"},
|
||||
"all_products": true,
|
||||
"limit_products": [],
|
||||
"include_pending": false,
|
||||
"send_date": null,
|
||||
"send_offset_days": 1,
|
||||
"send_offset_time": "18:00",
|
||||
"date_is_absolute": false,
|
||||
"offset_to_event_end": false,
|
||||
"offset_is_after": false,
|
||||
"send_to": "orders"
|
||||
}
|
||||
|
||||
: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)/sendmail_rules/
|
||||
|
||||
Create a new rule.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/sendmail_rules/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 166
|
||||
|
||||
{
|
||||
"enabled": true,
|
||||
"subject": {"en": "See you tomorrow!"},
|
||||
"template": {"en": "Don't forget your tickets, download them at {url}"},
|
||||
"all_products": true,
|
||||
"limit_products": [],
|
||||
"include_pending": false,
|
||||
"send_date": null,
|
||||
"send_offset_days": 1,
|
||||
"send_offset_time": "18:00",
|
||||
"date_is_absolute": false,
|
||||
"offset_to_event_end": false,
|
||||
"offset_is_after": false,
|
||||
"send_to": "orders"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"enabled": true,
|
||||
"subject": {"en": "See you tomorrow!"},
|
||||
"template": {"en": "Don't forget your tickets, download them at {url}"},
|
||||
"all_products": true,
|
||||
"limit_products": [],
|
||||
"include_pending": false,
|
||||
"send_date": null,
|
||||
"send_offset_days": 1,
|
||||
"send_offset_time": "18:00",
|
||||
"date_is_absolute": false,
|
||||
"offset_to_event_end": false,
|
||||
"offset_is_after": false,
|
||||
"send_to": "orders"
|
||||
}
|
||||
|
||||
: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)/sendmail_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/sendmail_rules/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 34
|
||||
|
||||
{
|
||||
"enabled": false,
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"enabled": false,
|
||||
"subject": {"en": "See you tomorrow!"},
|
||||
"template": {"en": "Don't forget your tickets, download them at {url}"},
|
||||
"all_products": true,
|
||||
"limit_products": [],
|
||||
"include_pending": false,
|
||||
"send_date": null,
|
||||
"send_offset_days": 1,
|
||||
"send_offset_time": "18:00",
|
||||
"date_is_absolute": false,
|
||||
"offset_to_event_end": false,
|
||||
"offset_is_after": false,
|
||||
"send_to": "orders"
|
||||
}
|
||||
|
||||
: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)/sendmail_rules/(id)/
|
||||
|
||||
Delete a rule.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v1/organizers/bigevents/events/sampleconf/sendmail_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.
|
||||
@@ -82,6 +82,10 @@ Endpoints
|
||||
|
||||
The sub-events resource can now be filtered by meta data attributes.
|
||||
|
||||
.. versionchanged:: 4.1
|
||||
|
||||
The ``with_availability_for`` parameter has been added.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/subevents/
|
||||
|
||||
Returns a list of all sub-events of an event.
|
||||
@@ -152,6 +156,10 @@ Endpoints
|
||||
only those sub-events having set their ``Format`` meta data to ``Seminar``, ``?attr[Format]=`` only those, that
|
||||
have no value set. Please note that this filter will respect default values set on
|
||||
organizer or event level.
|
||||
:query with_availability_for: If set to a sales channel identifier, the response will contain a special ``best_availability_state``
|
||||
attribute with values of 100 for "tickets available", values less than 100 for "tickets sold out or reserved",
|
||||
and ``null`` for "status unknown". These values might be served from a cache. This parameter can make the response
|
||||
slow.
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
||||
|
||||
@@ -16,15 +16,22 @@ Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal ID of the tax rule
|
||||
name multi-lingual string The tax rules' name
|
||||
internal_name string An optional name that is only used in the backend
|
||||
rate decimal (string) Tax rate in percent
|
||||
price_includes_tax boolean If ``true`` (default), tax is assumed to be included in
|
||||
the specified product price
|
||||
eu_reverse_charge boolean If ``true``, EU reverse charge rules are applied
|
||||
home_country string Merchant country (required for reverse charge), can be
|
||||
``null`` or empty string
|
||||
keep_gross_if_rate_changes boolean If ``true``, changes of the tax rate based on custom
|
||||
rules keep the gross price constant (default is ``false``)
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
.. versionchanged:: 4.6
|
||||
|
||||
The ``internal_name`` and ``keep_gross_if_rate_changes`` attributes have been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -56,9 +63,11 @@ Endpoints
|
||||
{
|
||||
"id": 1,
|
||||
"name": {"en": "VAT"},
|
||||
"internal_name": "VAT",
|
||||
"rate": "19.00",
|
||||
"price_includes_tax": true,
|
||||
"eu_reverse_charge": false,
|
||||
"keep_gross_if_rate_changes": false,
|
||||
"home_country": "DE"
|
||||
}
|
||||
]
|
||||
@@ -94,9 +103,11 @@ Endpoints
|
||||
{
|
||||
"id": 1,
|
||||
"name": {"en": "VAT"},
|
||||
"internal_name": "VAT",
|
||||
"rate": "19.00",
|
||||
"price_includes_tax": true,
|
||||
"eu_reverse_charge": false,
|
||||
"keep_gross_if_rate_changes": false,
|
||||
"home_country": "DE"
|
||||
}
|
||||
|
||||
@@ -140,9 +151,11 @@ Endpoints
|
||||
{
|
||||
"id": 1,
|
||||
"name": {"en": "VAT"},
|
||||
"internal_name": "VAT",
|
||||
"rate": "19.00",
|
||||
"price_includes_tax": true,
|
||||
"eu_reverse_charge": false,
|
||||
"keep_gross_if_rate_changes": false,
|
||||
"home_country": "DE"
|
||||
}
|
||||
|
||||
@@ -185,9 +198,11 @@ Endpoints
|
||||
{
|
||||
"id": 1,
|
||||
"name": {"en": "VAT"},
|
||||
"internal_name": "VAT",
|
||||
"rate": "20.00",
|
||||
"price_includes_tax": true,
|
||||
"eu_reverse_charge": false,
|
||||
"keep_gross_if_rate_changes": false,
|
||||
"home_country": "DE"
|
||||
}
|
||||
|
||||
|
||||
28
doc/development/algorithms/checkin.rst
Normal file
28
doc/development/algorithms/checkin.rst
Normal file
@@ -0,0 +1,28 @@
|
||||
.. spelling: libpretixsync
|
||||
|
||||
Check-in algorithms
|
||||
===================
|
||||
|
||||
When a ticket is scanned at the entrance or exit of an event, we follow a series of steps to determine whether
|
||||
the check-in is allowed or not. To understand some of the terms in the following diagrams, you should also check
|
||||
out the documentation of the :ref:`ticket redemption API endpoint <rest-checkin-redeem>`.
|
||||
|
||||
Server-side
|
||||
-----------
|
||||
|
||||
The following diagram shows the series of checks executed on the server when a ticket is redeemed through the API.
|
||||
Some simplifications have been made, for example the deduplication mechanism based on the ``nonce`` parameter
|
||||
to prevent re-uploads of the same scan is not shown.
|
||||
|
||||
.. image:: /images/checkin_online.png
|
||||
|
||||
Client-side
|
||||
-----------
|
||||
|
||||
The process of verifying tickets offline is a little different. There are two different approaches,
|
||||
depending on whether we have information about all tickets in the local database. The following diagram shows
|
||||
the algorithm as currently implemented in recent versions of `libpretixsync`_.
|
||||
|
||||
.. image:: /images/checkin_offline.png
|
||||
|
||||
.. _libpretixsync: https://github.com/pretix/libpretixsync
|
||||
13
doc/development/algorithms/index.rst
Normal file
13
doc/development/algorithms/index.rst
Normal file
@@ -0,0 +1,13 @@
|
||||
Algorithms
|
||||
==========
|
||||
|
||||
The business logic inside pretix is full of complex algorithms making decisions based on all the hundreds of settings
|
||||
and input parameters available. Some of them are documented here as graphs, either because fully understanding them is very important
|
||||
when working on features close to them, or because they also need to be re-implemented by client-side components like our
|
||||
ticket scanning apps and we want to ensure the implementations are as similar as possible to avoid confusion.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
checkin
|
||||
layouts
|
||||
15
doc/development/algorithms/layouts.rst
Normal file
15
doc/development/algorithms/layouts.rst
Normal file
@@ -0,0 +1,15 @@
|
||||
.. spelling: pretixPOS
|
||||
|
||||
Ticket layout
|
||||
=============
|
||||
|
||||
When a ticket is exported to PDF, the system needs to decide which of multiple PDF layouts to use. The
|
||||
following diagram shows the steps of the decision, showing both the implementation in pretix itself as
|
||||
well as the implementation in `pretixPOS`_.
|
||||
|
||||
The process can be influenced by plugins, which is demonstrated with the example of the shipping plugin.
|
||||
|
||||
.. image:: /images/ticket_layouts.png
|
||||
|
||||
|
||||
.. _pretixPOS: https://pretix.eu/about/en/pos
|
||||
@@ -20,20 +20,31 @@ Basically, three pre-defined flows are supported:
|
||||
* Authentication mechanisms that rely on **redirection**, e.g. to an OAuth provider. These can be implemented by
|
||||
supplying a ``authentication_url`` method and implementing a custom return view.
|
||||
|
||||
Authentication backends are *not* collected through a signal. Instead, they must explicitly be set through the
|
||||
``auth_backends`` directive in the ``pretix.cfg`` :ref:`configuration file <config>`.
|
||||
For security reasons, authentication backends are *not* automatically discovered through a signal. Instead, they must
|
||||
explicitly be set through the ``auth_backends`` directive in the ``pretix.cfg`` :ref:`configuration file <config>`.
|
||||
|
||||
In each of these methods (``form_authenticate``, ``request_authenticate`` or your custom view) you are supposed to
|
||||
either get an existing :py:class:`pretix.base.models.User` object from the database or create a new one. There are a
|
||||
few rules you need to follow:
|
||||
In each of these methods (``form_authenticate``, ``request_authenticate``, or your custom view) you are supposed to
|
||||
use ``User.objects.get_or_create_for_backend`` to get a :py:class:`pretix.base.models.User` object from the database
|
||||
or create a new one.
|
||||
|
||||
* You **MUST** only return users with the ``auth_backend`` attribute set to the ``identifier`` value of your backend.
|
||||
There are a few rules you need to follow:
|
||||
|
||||
* You **MUST** create new users with the ``auth_backend`` attribute set to the ``identifier`` value of your backend.
|
||||
* You **MUST** have some kind of identifier for a user that is globally unique and **SHOULD** never change, even if the
|
||||
user's name or email address changes. This could e.g. be the ID of the user in an external database. The identifier
|
||||
must not be longer than 190 characters. If you worry your backend might generated longer identifiers, consider
|
||||
using a hash function to trim them to a constant length.
|
||||
|
||||
* You **SHOULD** not allow users created by other authentication backends to log in through your code, and you **MUST**
|
||||
only create, modify or return users with ``auth_backend`` set to your backend.
|
||||
|
||||
* Every user object **MUST** have an email address. Email addresses are globally unique. If the email address is
|
||||
already registered to a user who signs in through a different backend, you **SHOULD** refuse the login.
|
||||
|
||||
``User.objects.get_or_create_for_backend`` will follow these rules for you automatically. It works like this:
|
||||
|
||||
.. autoclass:: pretix.base.models.auth.UserManager
|
||||
:members: get_or_create_for_backend
|
||||
|
||||
The backend interface
|
||||
---------------------
|
||||
|
||||
@@ -59,6 +70,7 @@ The backend interface
|
||||
|
||||
.. automethod:: authentication_url
|
||||
|
||||
|
||||
Logging users in
|
||||
----------------
|
||||
|
||||
@@ -68,3 +80,45 @@ recommend that you use the following utility method to correctly set session val
|
||||
authentication (if activated):
|
||||
|
||||
.. autofunction:: pretix.control.views.auth.process_login
|
||||
|
||||
A custom view that is called after a redirect from an external identity provider could look like this::
|
||||
|
||||
from django.contrib import messages
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
|
||||
from pretix.base.models import User
|
||||
from pretix.base.models.auth import EmailAddressTakenError
|
||||
from pretix.control.views.auth import process_login
|
||||
|
||||
|
||||
def return_view(request):
|
||||
# Verify validity of login with the external provider's API
|
||||
api_response = my_verify_login_function(
|
||||
code=request.GET.get('code')
|
||||
)
|
||||
|
||||
try:
|
||||
u = User.objects.get_or_create_for_backend(
|
||||
'my_backend_name',
|
||||
api_response['userid'],
|
||||
api_response['email'],
|
||||
set_always={
|
||||
'fullname': '{} {}'.format(
|
||||
api_response.get('given_name', ''),
|
||||
api_response.get('family_name', ''),
|
||||
),
|
||||
},
|
||||
set_on_creation={
|
||||
'locale': api_response.get('locale').lower()[:2],
|
||||
'timezone': api_response.get('zoneinfo', 'UTC'),
|
||||
}
|
||||
)
|
||||
except EmailAddressTakenError:
|
||||
messages.error(
|
||||
request, _('We cannot create your user account as a user account in this system '
|
||||
'already exists with the same email address.')
|
||||
)
|
||||
return redirect(reverse('control:auth.login'))
|
||||
else:
|
||||
return process_login(request, u, keep_logged_in=False)
|
||||
|
||||
119
doc/development/api/cookieconsent.rst
Normal file
119
doc/development/api/cookieconsent.rst
Normal file
@@ -0,0 +1,119 @@
|
||||
.. highlight:: python
|
||||
:linenothreshold: 5
|
||||
|
||||
.. _`cookieconsent`:
|
||||
|
||||
Handling cookie consent
|
||||
=======================
|
||||
|
||||
pretix includes an optional feature to handle cookie consent explicitly to comply with EU regulations.
|
||||
If your plugin sets non-essential cookies or includes a third-party service that does so, you should
|
||||
integrate with this feature.
|
||||
|
||||
Server-side integration
|
||||
-----------------------
|
||||
|
||||
First, you need to declare that you are using non-essential cookies by responding to the following
|
||||
signal:
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
:members: register_cookie_providers
|
||||
|
||||
You are expected to return a list of ``CookieProvider`` objects instantiated from the following class:
|
||||
|
||||
.. class:: pretix.presale.cookies.CookieProvider
|
||||
|
||||
.. py:attribute:: CookieProvider.identifier
|
||||
|
||||
A short and unique identifier used to distinguish this cookie provider form others (required).
|
||||
|
||||
.. py:attribute:: CookieProvider.provider_name
|
||||
|
||||
A human-readable name of the entity of feature responsible for setting the cookie (required).
|
||||
|
||||
.. py:attribute:: CookieProvider.usage_classes
|
||||
|
||||
A list of enum values from the ``pretix.presale.cookies.UsageClass`` enumeration class, such as
|
||||
``UsageClass.ANALYTICS``, ``UsageClass.MARKETING``, or ``UsageClass.SOCIAL`` (required).
|
||||
|
||||
.. py:attribute:: CookieProvider.privacy_url
|
||||
|
||||
A link to a privacy policy (optional).
|
||||
|
||||
Here is an example of such a receiver:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@receiver(register_cookie_providers)
|
||||
def recv_cookie_providers(sender, request, **kwargs):
|
||||
return [
|
||||
CookieProvider(
|
||||
identifier='google_analytics',
|
||||
provider_name='Google Analytics',
|
||||
usage_classes=[UsageClass.ANALYTICS],
|
||||
)
|
||||
]
|
||||
|
||||
JavaScript-side integration
|
||||
---------------------------
|
||||
|
||||
The server-side integration only causes the cookie provider to show up in the cookie dialog. You still
|
||||
need to care about actually enforcing the consent state.
|
||||
|
||||
You can access the consent state through the ``window.pretix.cookie_consent`` variable. Whenever the
|
||||
value changes, a ``pretix:cookie-consent:change`` event is fired on the ``document`` object.
|
||||
|
||||
The variable will generally have one of the following states:
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
================================================================ =====================================================
|
||||
State Interpretation
|
||||
================================================================ =====================================================
|
||||
``pretix === undefined || pretix.cookie_consent === undefined`` Your JavaScript has loaded before the cookie consent
|
||||
script. Wait for the event to be fired, then try again,
|
||||
do not yet set a cookie.
|
||||
``pretix.cookie_consent === null`` The cookie consent mechanism has not been enabled. This
|
||||
usually means that you can set cookies however you like.
|
||||
``pretix.cookie_consent[identifier] === undefined`` The cookie consent mechanism is loaded, but has no data
|
||||
on your cookie yet, wait for the event to be fired, do not
|
||||
yet set a cookie.
|
||||
``pretix.cookie_consent[identifier] === true`` The user has consented to your cookie.
|
||||
``pretix.cookie_consent[identifier] === false`` The user has actively rejected your cookie.
|
||||
================================================================ =====================================================
|
||||
|
||||
If you are integrating e.g. a tracking provider with native cookie consent support such
|
||||
as Facebook's Pixel, you can integrate it like this:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var consent = (window.pretix || {}).cookie_consent;
|
||||
if (consent !== null && !(consent || {}).facebook) {
|
||||
fbq('consent', 'revoke');
|
||||
}
|
||||
fbq('init', ...);
|
||||
document.addEventListener('pretix:cookie-consent:change', function (e) {
|
||||
fbq('consent', (e.detail || {}).facebook ? 'grant' : 'revoke');
|
||||
})
|
||||
|
||||
If you have a JavaScript function that you only want to load if consent for a specific ``identifier``
|
||||
is given, you can wrap it like this:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var consent_identifier = "youridentifier";
|
||||
var consent = (window.pretix || {}).cookie_consent;
|
||||
if (consent === null || (consent || {})[consent_identifier] === true) {
|
||||
// Cookie consent tool is either disabled or consent is given
|
||||
addScriptElement(src);
|
||||
return;
|
||||
}
|
||||
|
||||
// Either cookie consent tool has not loaded yet or consent is not given
|
||||
document.addEventListener('pretix:cookie-consent:change', function onChange(e) {
|
||||
var consent = e.detail || {};
|
||||
if (consent === null || consent[consent_identifier] === true) {
|
||||
addScriptElement(src);
|
||||
document.removeEventListener('pretix:cookie-consent:change', onChange);
|
||||
}
|
||||
})
|
||||
@@ -17,6 +17,7 @@ Contents:
|
||||
shredder
|
||||
import
|
||||
customview
|
||||
cookieconsent
|
||||
auth
|
||||
general
|
||||
quality
|
||||
|
||||
@@ -62,6 +62,8 @@ The provider class
|
||||
|
||||
.. autoattribute:: public_name
|
||||
|
||||
.. autoattribute:: confirm_button_name
|
||||
|
||||
.. autoattribute:: is_enabled
|
||||
|
||||
.. autoattribute:: priority
|
||||
|
||||
@@ -45,13 +45,17 @@ Attribute Type Description
|
||||
name string The human-readable name of your plugin
|
||||
author string Your name
|
||||
version string A human-readable version code of your plugin
|
||||
description string A more verbose description of what your plugin does.
|
||||
description string A more verbose description of what your plugin does. May contain HTML.
|
||||
category string Category of a plugin. Either one of ``"FEATURE"``, ``"PAYMENT"``,
|
||||
``"INTEGRATION"``, ``"CUSTOMIZATION"``, ``"FORMAT"``, or ``"API"``,
|
||||
or any other string.
|
||||
picture string (optional) Path to a picture resolvable through the static file system.
|
||||
featured boolean (optional) ``False`` by default, can promote a plugin if it's something many users will want, use carefully.
|
||||
visible boolean (optional) ``True`` by default, can hide a plugin so it cannot be normally activated.
|
||||
restricted boolean (optional) ``False`` by default, restricts a plugin such that it can only be enabled
|
||||
for an event by system administrators / superusers.
|
||||
experimental boolean (optional) ``False`` by default, marks a plugin as an experimental feature in the plugins list.
|
||||
picture string (optional) Path to a picture resolvable through the static file system.
|
||||
compatibility string Specifier for compatible pretix versions.
|
||||
================== ==================== ===========================================================
|
||||
|
||||
@@ -74,8 +78,10 @@ A working example would be:
|
||||
name = _("PayPal")
|
||||
author = _("the pretix team")
|
||||
version = '1.0.0'
|
||||
category = 'PAYMENT
|
||||
category = 'PAYMENT'
|
||||
picture = 'pretix_paypal/paypal_logo.svg'
|
||||
visible = True
|
||||
featured = False
|
||||
restricted = False
|
||||
description = _("This plugin allows you to receive payments via PayPal")
|
||||
compatibility = "pretix>=2.7.0"
|
||||
@@ -92,6 +98,7 @@ those will be displayed but not block the plugin execution.
|
||||
|
||||
The ``AppConfig`` class may implement a method ``is_available(event)`` that checks if a plugin
|
||||
is available for a specific event. If not, it will not be shown in the plugin list of that event.
|
||||
You should not define ``is_available`` and ``restricted`` on the same plugin.
|
||||
|
||||
Plugin registration
|
||||
-------------------
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
.. spelling:: Rebase rebasing
|
||||
|
||||
Coding style and quality
|
||||
========================
|
||||
|
||||
Code
|
||||
----
|
||||
|
||||
* Basically, we want all python code to follow the `PEP 8`_ standard. There are a few exceptions where
|
||||
we see things differently or just aren't that strict. The ``setup.cfg`` file in the project's source
|
||||
folder contains definitions that allow `flake8`_ to check for violations automatically. See :ref:`checksandtests`
|
||||
@@ -20,8 +25,62 @@ Coding style and quality
|
||||
test suite are in the style of Python's unit test module. If you extend those files, you might continue in this style,
|
||||
but please use ``pytest`` style for any new test files.
|
||||
|
||||
* Please keep the first line of your commit messages short. When referencing an issue, please phrase it like
|
||||
``Fix #123 -- Problems with order creation`` or ``Refs #123 -- Fix this part of that bug``.
|
||||
Commits and Pull Requests
|
||||
-------------------------
|
||||
|
||||
|
||||
|
||||
Most commits should start as pull requests, therefore this applies to the titles of pull requests as well since
|
||||
the pull request title will become the commit message on merge. We prefer merging with GitHub's "Squash and merge"
|
||||
feature if the PR contains multiple commits that do not carry value to keep. If there is value in keeping the
|
||||
individual commits, we use "Rebase and merge" instead. Merge commits should be avoided.
|
||||
|
||||
* The commit message should start with a single subject line and can optionally be followed by a commit message body.
|
||||
|
||||
* The subject line should be the shortest possible representation of what the commit changes. Someone who reviewed
|
||||
the commit should able to immediately remember the commit in a couple of weeks based on the subject line and tell
|
||||
it apart from other commits.
|
||||
|
||||
* If there's additional useful information that we should keep, such as reasoning behind the commit, you can
|
||||
add a longer body, separated from the first line by a blank line.
|
||||
|
||||
* The body should explain **what** you changed and more importantly **why** you changed it. There's no need to iterate
|
||||
**how** you changed something.
|
||||
|
||||
* The subject line should be capitalized ("Add new feature" instead of "add new feature") and should not end with a period
|
||||
("Add new feature" instead of "Add new feature.")
|
||||
|
||||
* The subject line should be written in imperative mood, as if you were giving a command what the computer should do if the
|
||||
commit is applied. This is how generated commit messages by git itself are already written ("Merge branch …", "Revert …")
|
||||
and makes for short and consistent messages.
|
||||
|
||||
* Good: "Fix typo in template"
|
||||
* Good: "Add Chinese translation"
|
||||
* Good: "Remove deprecated method"
|
||||
* Good: "Bump version to 4.4.0"
|
||||
* Bad: "Fixed bug with …"
|
||||
* Bad: "Fixes bug with …"
|
||||
* Bad: "Fixing bug …"
|
||||
|
||||
* If all changes in your commit are in context of a single feature or e.g. a bundled plugin, it makes sense to prefix the
|
||||
subject line with the name of that feature. Examples:
|
||||
|
||||
* "API: Add support for PATCH on customers"
|
||||
* "Docs: Add chapter on alpaca feeding"
|
||||
* "Stripe: Fix duplicate payments"
|
||||
* "Order change form: Fix incorrect validation"
|
||||
|
||||
* If your commit references a GitHub issue that is fully resolved by your commit, start your subject line with the issue
|
||||
ID in the form of "Fix #1234 -- Crash in order list". In this case, you can omit the verb "Fix" at the beginning of the
|
||||
second part of the message to avoid repetition of the word "fix". If your commit only partially resolves the issue, use
|
||||
"Refs #1234 -- Crash in order list" instead.
|
||||
|
||||
* Applies to pretix employees only: If your commit references a sentry issue, please put it in parentheses at the end
|
||||
of the subject line or inside the body ("Fix crash in order list (PRETIXEU-ABC)"). If your commit references a support
|
||||
ticket, please put it in parentheses at the end of the subject line with a "Z#" prefix ("Fix crash in order list (Z#12345)").
|
||||
|
||||
* If your PR was open for a while and might cause conflicts on merge, please prefer rebasing it (``git rebase -i master``)
|
||||
over merging ``master`` into your branch unless it is prohibitively complicated.
|
||||
|
||||
|
||||
.. _PEP 8: https://legacy.python.org/dev/peps/pep-0008/
|
||||
|
||||
@@ -92,6 +92,9 @@ Carts and Orders
|
||||
.. autoclass:: pretix.base.models.OrderRefund
|
||||
:members:
|
||||
|
||||
.. autoclass:: pretix.base.models.Transaction
|
||||
:members:
|
||||
|
||||
.. autoclass:: pretix.base.models.CartPosition
|
||||
:members:
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ Developer documentation
|
||||
setup
|
||||
contribution/index
|
||||
implementation/index
|
||||
translation/index
|
||||
algorithms/index
|
||||
api/index
|
||||
structure
|
||||
translation/index
|
||||
|
||||
@@ -26,7 +26,7 @@ Your should install the following on your system:
|
||||
* ``libssl`` (Debian package: ``libssl-dev``)
|
||||
* ``libxml2`` (Debian package ``libxml2-dev``)
|
||||
* ``libxslt`` (Debian package ``libxslt1-dev``)
|
||||
* ``libenchant1c2a`` (Debian package ``libenchant1c2a``)
|
||||
* ``libenchant-2-2`` (Debian package ``libenchant-2-2``)
|
||||
* ``msgfmt`` (Debian package ``gettext``)
|
||||
* ``git``
|
||||
|
||||
@@ -51,10 +51,15 @@ the dependencies might fail::
|
||||
|
||||
Working with the code
|
||||
---------------------
|
||||
The first thing you need are all the main application's dependencies::
|
||||
If you do not have a recent installation of ``nodejs``, install it now::
|
||||
|
||||
curl -sL https://deb.nodesource.com/setup_17.x | sudo -E bash -
|
||||
sudo apt install nodejs
|
||||
|
||||
To make sure it is on your path variable, close and reopen your terminal. Now, install the Python-level dependencies of pretix::
|
||||
|
||||
cd src/
|
||||
pip3 install -r requirements.txt -r requirements/dev.txt
|
||||
pip3 install -e ".[dev]"
|
||||
|
||||
Next, you need to copy the SCSS files from the source folder to the STATIC_ROOT directory::
|
||||
|
||||
|
||||
BIN
doc/images/checkin_offline.png
Normal file
BIN
doc/images/checkin_offline.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 236 KiB |
146
doc/images/checkin_offline.puml
Normal file
146
doc/images/checkin_offline.puml
Normal file
@@ -0,0 +1,146 @@
|
||||
@startuml
|
||||
|
||||
|
||||
partition "data-based check" {
|
||||
"Check based on local database" --> "Is the order in status PAID or PENDING\nand is the position not canceled?"
|
||||
--> if "" then
|
||||
-right->[no] "Return error CANCELED"
|
||||
else
|
||||
-down->[yes] "Is the product part of the check-in list?"
|
||||
--> if "" then
|
||||
-right->[no] "Return error PRODUCT"
|
||||
else
|
||||
-down->[yes] "Is the subevent part of the check-in list?"
|
||||
--> if "" then
|
||||
-right->[no] "Return error INVALID"
|
||||
note bottom: TODO\ninconsistent\nwith online\ncheck
|
||||
else
|
||||
-down->[yes] "Is the order in status PAID?"
|
||||
--> if "" then
|
||||
-right->[no] "Does the check-in list include pending orders?"
|
||||
--> if "" then
|
||||
-right->[no] "Return error UNPAID "
|
||||
else
|
||||
-down->[yes] "Is ignore_unpaid set?\n(Has the operator confirmed\nthe checkin?)"
|
||||
--> if "" then
|
||||
-right->[no] "Return error UNPAID "
|
||||
else
|
||||
-down->[yes] "Is this an entry or exit?"
|
||||
endif
|
||||
endif
|
||||
else
|
||||
-down->[yes] "Is this an entry or exit?"
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
"Is this an entry or exit?" --> if "" then
|
||||
-right->[entry] Evaluate custom logic (rules)
|
||||
--> if "" then
|
||||
-right->[error] "Return error RULES"
|
||||
else
|
||||
-down->[ok] "Are all required questions answered?"
|
||||
--> if "" then
|
||||
-right->[no] "Return error INCOMPLETE"
|
||||
else
|
||||
-down->[yes] "Does the check-in list allow multi-entry?"
|
||||
endif
|
||||
endif
|
||||
else
|
||||
-->[exit] "Return OK "
|
||||
endif
|
||||
|
||||
"Does the check-in list allow multi-entry?" --> if "" then
|
||||
-right->[yes] "Return OK"
|
||||
else
|
||||
-down->[no] "Is this the first checkin\nfor this ticket on this list?"
|
||||
--> if "" then
|
||||
-right->[yes] "Return OK"
|
||||
else
|
||||
-down->[no] "Are all previous checkins\nfor this ticket on this list exits?"
|
||||
--> if "" then
|
||||
-right->[yes] "Return OK"
|
||||
else
|
||||
-down->[no] "Does the check-in list\n allow entry after exit\nand is the last checkin\nan exit?"
|
||||
--> if "" then
|
||||
-right->[yes] "Return OK"
|
||||
else
|
||||
-down->[no] "Return error ALREADY_REDEEMED"
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
}
|
||||
|
||||
partition "dataless check" {
|
||||
"Check based on secret content" --> "Does the secret decode with\nany supported scheme\nand has a valid signature?"
|
||||
|
||||
--> if "" then
|
||||
-down->[yes] "Is the ticket secret on the revocation list?"
|
||||
--> if "" then
|
||||
-right->[yes] "Return error REVOKED"
|
||||
else
|
||||
-down->[no] "Is the product part of the check-in list? "
|
||||
--> if "" then
|
||||
-right->[no] "Return error PRODUCT "
|
||||
else
|
||||
-down->[yes] "Is the subevent part of the check-in list? "
|
||||
--> if "" then
|
||||
-right->[no] "Return error INVALID "
|
||||
note bottom: TODO\ninconsistent\nwith online\ncheck
|
||||
else
|
||||
--> "Is this an entry or exit? "
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
else
|
||||
-right>[no] "Return error INVALID "
|
||||
endif
|
||||
|
||||
"Is this an entry or exit? " --> if "" then
|
||||
-right->[entry] "Evaluate custom logic (rules) "
|
||||
--> if "" then
|
||||
-right->[error] "Return error RULES "
|
||||
else
|
||||
-down->[ok] "Are all required questions answered? "
|
||||
--> if "" then
|
||||
-right->[no] "Return error INCOMPLETE "
|
||||
else
|
||||
-down->[yes] "Does the check-in list allow multi-entry? "
|
||||
endif
|
||||
endif
|
||||
else
|
||||
-->[exit] " Return OK "
|
||||
endif
|
||||
|
||||
"Does the check-in list allow multi-entry? " --> if "" then
|
||||
-right->[yes] " Return OK "
|
||||
else
|
||||
-down->[no] "Are any locally queued checkins for\nthis ticket of this list known?"
|
||||
--> if "" then
|
||||
-right->[no] " Return OK "
|
||||
else
|
||||
-down->[yes] "Are all locally queued checkins\nfor this ticket on this list exits? "
|
||||
--> if "" then
|
||||
-right->[yes] " Return OK "
|
||||
else
|
||||
-down->[no] "Does the check-in list\n allow entry after exit\nand is the last locally\nqueued checkin\nan exit? "
|
||||
--> if "" then
|
||||
-right->[yes] " Return OK "
|
||||
else
|
||||
-down->[no] "Return error ALREADY_REDEEMED "
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
}
|
||||
|
||||
(*) --> "Check if order position with\nscanned ticket secret exists"
|
||||
--> if "" then
|
||||
-down->[yes] "Check based on local database"
|
||||
else
|
||||
-->[no] "Check based on secret content"
|
||||
endif
|
||||
|
||||
@enduml
|
||||
BIN
doc/images/checkin_online.png
Normal file
BIN
doc/images/checkin_online.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 147 KiB |
92
doc/images/checkin_online.puml
Normal file
92
doc/images/checkin_online.puml
Normal file
@@ -0,0 +1,92 @@
|
||||
@startuml
|
||||
|
||||
(*) --> "Check if order position with\nscanned ticket secret exists"
|
||||
--> if "" then
|
||||
-down->[yes] ===CHECK===
|
||||
else
|
||||
-->[no] "Check if secret exists\nin revocation list"
|
||||
--> if "" then
|
||||
--> "Is this a forced upload?"
|
||||
--> if "" then
|
||||
-->[yes] ===CHECK===
|
||||
else
|
||||
-right->[no] "Return error REVOKED"
|
||||
endif
|
||||
else
|
||||
-right->[no] "Return error INVALID"
|
||||
endif
|
||||
|
||||
endif
|
||||
|
||||
|
||||
===CHECK=== -down-> "Is the order in status PAID or PENDING\nand is the position not canceled?"
|
||||
--> if "" then
|
||||
-right->[no] "Return error CANCELED"
|
||||
else
|
||||
-down->[yes] "Is the product part of the check-in list?"
|
||||
--> if "" then
|
||||
-right->[no] "Return error PRODUCT"
|
||||
else
|
||||
-down->[yes] "Is the subevent part of the check-in list?"
|
||||
--> if "" then
|
||||
-right->[no] "Return error PRODUCT "
|
||||
else
|
||||
-down->[yes] "Is the order in status PAID\nor is this a forced upload?"
|
||||
--> if "" then
|
||||
-right->[no] "Does the check-in list include pending orders?"
|
||||
--> if "" then
|
||||
-right->[no] "Return error UNPAID "
|
||||
else
|
||||
-down->[yes] "Is ignore_unpaid set?\n(Has the operator confirmed\nthe checkin?)"
|
||||
--> if "" then
|
||||
-right->[no] "Return error UNPAID "
|
||||
else
|
||||
-down->[yes] "Is this an entry or exit?\nIs the upload forced?"
|
||||
endif
|
||||
endif
|
||||
else
|
||||
-down->[yes] "Is this an entry or exit?\nIs the upload forced?"
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
"Is this an entry or exit?\nIs the upload forced?" --> if "" then
|
||||
-right->[entry && not force] Evaluate custom logic (rules)
|
||||
--> if "" then
|
||||
-right->[error] "Return error RULES"
|
||||
else
|
||||
-down->[ok] "Are all required questions answered?"
|
||||
--> if "" then
|
||||
-right->[no && questions_supported] "Return error INCOMPLETE"
|
||||
else
|
||||
-down->[yes || not questions_supported] "Does the check-in list allow multi-entry?"
|
||||
endif
|
||||
endif
|
||||
else
|
||||
-->[exit || force=true] "Return OK "
|
||||
endif
|
||||
|
||||
"Does the check-in list allow multi-entry?" --> if "" then
|
||||
-right->[yes] "Return OK"
|
||||
else
|
||||
-down->[no] "Is this the first checkin\nfor this ticket on this list?"
|
||||
--> if "" then
|
||||
-right->[yes] "Return OK"
|
||||
else
|
||||
-down->[no] "Are all previous checkins\nfor this ticket on this list exits?"
|
||||
--> if "" then
|
||||
-right->[yes] "Return OK"
|
||||
else
|
||||
-down->[no] "Does the check-in list\n allow entry after exit\nand is the last checkin\nan exit?"
|
||||
--> if "" then
|
||||
-right->[yes] "Return OK"
|
||||
else
|
||||
-down->[no] "Return error ALREADY_REDEEMED"
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
|
||||
@enduml
|
||||
BIN
doc/images/ticket_layouts.png
Normal file
BIN
doc/images/ticket_layouts.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
52
doc/images/ticket_layouts.puml
Normal file
52
doc/images/ticket_layouts.puml
Normal file
@@ -0,0 +1,52 @@
|
||||
@startuml
|
||||
|
||||
(*) --> "Which implementation?"
|
||||
--> if "" then
|
||||
-down->[pretixPOS] "Check for TicketLayoutItem with\nsales_channel=pretixpos [libpretixsync]"
|
||||
--> if "" then
|
||||
--> (*)
|
||||
else
|
||||
-->[not found] "Check for TicketLayoutItem with\nsales_channel=web [libpretixsync]"
|
||||
--> if "" then
|
||||
--> (*)
|
||||
else
|
||||
-->[not found] "Use event default [libpretixsync]"
|
||||
--> (*)
|
||||
endif
|
||||
endif
|
||||
|
||||
else
|
||||
-right->[pretix] "Check for TicketLayoutItem with\nsales_channel=order.sales_channel"
|
||||
--> if "" then
|
||||
-right-> "Run override_layout plugin signal on result"
|
||||
else
|
||||
-down->[not found] "Check for TicketLayoutItem with\nsales_channel=web"
|
||||
--> if "" then
|
||||
--> "Run override_layout plugin signal on result"
|
||||
else
|
||||
-->[not found] "Use event default"
|
||||
--> "Run override_layout plugin signal on result"
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
|
||||
"Run override_layout plugin signal on result" -> (*)
|
||||
|
||||
|
||||
partition pretix_shipping {
|
||||
"Run override_layout plugin signal on result" --> "Check for ShippingLayoutItem with\nmethod=order.shipping_method"
|
||||
--> if "" then
|
||||
--> (*)
|
||||
else
|
||||
-down->[not found] "Check for ShippingMethod.layout"
|
||||
--> if "" then
|
||||
--> (*)
|
||||
else
|
||||
-down->[not found] "Keep original layout"
|
||||
--> (*)
|
||||
endif
|
||||
endif
|
||||
}
|
||||
|
||||
@enduml
|
||||
64
doc/plugins/certificates.rst
Normal file
64
doc/plugins/certificates.rst
Normal file
@@ -0,0 +1,64 @@
|
||||
Certificates of attendance
|
||||
==========================
|
||||
|
||||
The certificates plugin provides a HTTP API that allows you to download the certificate for a specific attendee.
|
||||
|
||||
|
||||
Certificate download
|
||||
--------------------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/certificate/
|
||||
|
||||
Downloads the certificate for one order position, identified by its internal ID. Download is a two-step
|
||||
process. You will always get a :http:statuscode:`303` response with a ``Location`` header to a different
|
||||
URL. In the background, our server starts preparing the PDF file.
|
||||
|
||||
If you then do a ``GET`` to the URL you were given, you will either receive a :http:statuscode:`409` response
|
||||
indicating to retry after a few seconds, or a :http:statuscode:`200` response with the PDF file.
|
||||
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/download/certificate/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 303 See Other
|
||||
Location: /api/v1/organizers/democon/events/3vjrh/orderpositions/426/certificate/?result=1f550651-ae7b-4911-a76c-2be8f348aaa5
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/download/certificate/?result=1f550651-ae7b-4911-a76c-2be8f348aaa5 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/pdf
|
||||
|
||||
...
|
||||
|
||||
: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 order position to fetch
|
||||
:statuscode 200: File ready for download
|
||||
:statuscode 303: Processing started
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource
|
||||
**or** downloads are not available for this order position at this time. The response content will
|
||||
contain more details.
|
||||
:statuscode 404: The requested order position or download provider does not exist.
|
||||
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting for a few
|
||||
seconds.
|
||||
@@ -61,7 +61,7 @@ Variable Description
|
||||
``attendee_city`` City of the ticket holder's address (or empty)
|
||||
``attendee_country`` Country code of the ticket holder's address (or empty)
|
||||
``attendee_state`` State of the ticket holder's address (or empty)
|
||||
``answer[XYZ]`` Answer to the custom question with identifier ``XYZ``
|
||||
``answers[XYZ]`` Answer to the custom question with identifier ``XYZ``
|
||||
``invoice_name`` Full name of the invoice address (or empty)
|
||||
``invoice_name_*`` Name parts of the invoice address, depending on configuration, e.g. ``invoice_name_given_name`` or ``invoice_name_family_name``
|
||||
``invoice_company`` Company of the invoice address (or empty)
|
||||
|
||||
562
doc/plugins/exhibitors.rst
Normal file
562
doc/plugins/exhibitors.rst
Normal file
@@ -0,0 +1,562 @@
|
||||
Exhibitors
|
||||
==========
|
||||
|
||||
The exhibitors plugin allows to manage exhibitors at your trade show or conference. After signing up your exhibitors
|
||||
in the system, you can assign vouchers to exhibitors and give them access to the data of these vouchers. The exhibitors
|
||||
module is also the basis of the pretixLEAD lead scanning application.
|
||||
|
||||
.. note:: On pretix Hosted, using the lead scanning feature of the exhibitors plugin can add additional costs
|
||||
depending on your contract.
|
||||
|
||||
The plugin exposes two APIs. One (REST API) is intended for bulk-data operations from the admin side, and one
|
||||
(App API) that is used by the pretixLEAD app.
|
||||
|
||||
REST API
|
||||
---------
|
||||
|
||||
The REST API for exhibitors requires the usual :ref:`rest-auth`.
|
||||
|
||||
Resources
|
||||
"""""""""
|
||||
|
||||
The exhibitors plugin provides a HTTP API that allows you to create new exhibitors.
|
||||
|
||||
The exhibitors resource contains the following public fields:
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
===================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal exhibitor ID in pretix
|
||||
name string Exhibitor name
|
||||
internal_id string Can be used for the ID in your exhibition system, your customer ID, etc. Can be ``null``. Maximum 255 characters.
|
||||
contact_name string Contact person (or ``null``)
|
||||
contact_name_parts object of strings Decomposition of contact name (i.e. given name, family name)
|
||||
contact_email string Contact person email address (or ``null``)
|
||||
booth string Booth number (or ``null``). Maximum 100 characters.
|
||||
locale string Locale for communication with the exhibitor (or ``null``).
|
||||
access_code string Access code for the exhibitor to access their data or use the lead scanning app (read-only).
|
||||
allow_lead_scanning boolean Enables lead scanning app
|
||||
allow_lead_access boolean Enables access to data gathered by the lead scanning app
|
||||
allow_voucher_access boolean Enables access to data gathered by exhibitor vouchers
|
||||
comment string Internal comment, not shown to exhibitor
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
You can also access the scanned leads through the API which contains the following public fields:
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
===================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
attendee_order string Order code of the order the scanned attendee belongs to
|
||||
attendee_positionid integer ``positionid`` if the attendee within the order specified by ``attendee_order``
|
||||
rating integer A rating of 0 to 5 stars (or ``null``)
|
||||
notes string A note taken by the exhibitor after scanning
|
||||
tags list of strings Additional tags selected by the exhibitor
|
||||
first_upload datetime Date and time of the first upload of this lead
|
||||
data list of objects Attendee data set that may be shown to the exhibitor based o
|
||||
the event's configuration. Each entry contains the fields ``id``, ``label``, and ``value``.
|
||||
device_name string User-defined name for the device used for scanning (or ``null``).
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
Endpoints
|
||||
"""""""""
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/exhibitors/
|
||||
|
||||
Returns a list of all exhibitors configured for an event.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/exhibitors/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Aperture Science",
|
||||
"internal_id": null,
|
||||
"contact_name": "Dr Cave Johnson",
|
||||
"contact_name_parts": {
|
||||
"_scheme": "salutation_title_given_family",
|
||||
"family_name": "Johnson",
|
||||
"given_name": "Cave",
|
||||
"salutation": "",
|
||||
"title": "Dr"
|
||||
},
|
||||
"contact_email": "johnson@as.example.org",
|
||||
"booth": "A2",
|
||||
"locale": "de",
|
||||
"access_code": "VKHZ2FU8",
|
||||
"allow_lead_scanning": true,
|
||||
"allow_lead_access": true,
|
||||
"allow_voucher_access": true,
|
||||
"comment": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query page: The page number in case of a multi-page result set, default is 1
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to view it.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/exhibitors/(id)/
|
||||
|
||||
Returns information on one exhibitor, identified by its ID.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/exhibitors/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Aperture Science",
|
||||
"internal_id": null,
|
||||
"contact_name": "Dr Cave Johnson",
|
||||
"contact_name_parts": {
|
||||
"_scheme": "salutation_title_given_family",
|
||||
"family_name": "Johnson",
|
||||
"given_name": "Cave",
|
||||
"salutation": "",
|
||||
"title": "Dr"
|
||||
},
|
||||
"contact_email": "johnson@as.example.org",
|
||||
"booth": "A2",
|
||||
"locale": "de",
|
||||
"access_code": "VKHZ2FU8",
|
||||
"allow_lead_scanning": true,
|
||||
"allow_lead_access": true,
|
||||
"allow_voucher_access": true,
|
||||
"comment": ""
|
||||
}
|
||||
|
||||
: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 exhibitor to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event/exhibitor does not exist **or** you have no permission to view it.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/exhibitors/(id)/leads/
|
||||
|
||||
Returns a list of all scanned leads of an exhibitor.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/exhibitors/1/leads/ 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": [
|
||||
{
|
||||
"attendee_order": "T0E7E",
|
||||
"attendee_positionid": 1,
|
||||
"rating": 1,
|
||||
"notes": "",
|
||||
"tags": [],
|
||||
"first_upload": "2021-07-06T11:03:31.414491+01:00",
|
||||
"data": [
|
||||
{
|
||||
"id": "attendee_name",
|
||||
"label": "Attendee name",
|
||||
"value": "Peter",
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
: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
|
||||
:param id: The ``id`` field of the exhibitor to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer or event or exhibitor does not exist **or** you have no permission to view it.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/exhibitors/
|
||||
|
||||
Create a new exhibitor.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/exhibitors/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 166
|
||||
|
||||
{
|
||||
"name": "Aperture Science",
|
||||
"internal_id": null,
|
||||
"contact_name_parts": {
|
||||
"_scheme": "salutation_title_given_family",
|
||||
"family_name": "Johnson",
|
||||
"given_name": "Cave",
|
||||
"salutation": "",
|
||||
"title": "Dr"
|
||||
},
|
||||
"contact_email": "johnson@as.example.org",
|
||||
"booth": "A2",
|
||||
"locale": "de",
|
||||
"access_code": "VKHZ2FU8",
|
||||
"allow_lead_scanning": true,
|
||||
"allow_lead_access": true,
|
||||
"allow_voucher_access": true,
|
||||
"comment": ""
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Aperture Science",
|
||||
"internal_id": null,
|
||||
"contact_name": "Dr Cave Johnson",
|
||||
"contact_name_parts": {
|
||||
"_scheme": "salutation_title_given_family",
|
||||
"family_name": "Johnson",
|
||||
"given_name": "Cave",
|
||||
"salutation": "",
|
||||
"title": "Dr"
|
||||
},
|
||||
"contact_email": "johnson@as.example.org",
|
||||
"booth": "A2",
|
||||
"locale": "de",
|
||||
"access_code": "VKHZ2FU8",
|
||||
"allow_lead_scanning": true,
|
||||
"allow_lead_access": true,
|
||||
"allow_voucher_access": true,
|
||||
"comment": ""
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to create new exhibitor for
|
||||
:param event: The ``slug`` field of the event to create new exhibitor for
|
||||
:statuscode 201: no error
|
||||
:statuscode 400: The exhibitor 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 exhibitors.
|
||||
|
||||
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/exhibitors/(id)/
|
||||
|
||||
Update an exhibitor. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
|
||||
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
|
||||
want to change.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 34
|
||||
|
||||
{
|
||||
"internal_id": "ABC"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Aperture Science",
|
||||
"internal_id": "ABC",
|
||||
"contact_name": "Dr Cave Johnson",
|
||||
"contact_name_parts": {
|
||||
"_scheme": "salutation_title_given_family",
|
||||
"family_name": "Johnson",
|
||||
"given_name": "Cave",
|
||||
"salutation": "",
|
||||
"title": "Dr"
|
||||
},
|
||||
"contact_email": "johnson@as.example.org",
|
||||
"booth": "A2",
|
||||
"locale": "de",
|
||||
"access_code": "VKHZ2FU8",
|
||||
"allow_lead_scanning": true,
|
||||
"allow_lead_access": true,
|
||||
"allow_voucher_access": true,
|
||||
"comment": ""
|
||||
}
|
||||
|
||||
: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 exhibitor to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The exhibitor could not be modified due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event/exhibitor does not exist **or** you have no permission to change it.
|
||||
|
||||
|
||||
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/exhibitors/(id)/
|
||||
|
||||
Delete an exhibitor.
|
||||
|
||||
.. warning:: This deletes all lead scan data and removes all connections to vouchers (the vouchers are not deleted).
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v1/organizers/bigevents/events/sampleconf/exhibitors/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 exhibitor to delete
|
||||
:statuscode 204: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event/exhibitor does not exist **or** you have no permission to change it
|
||||
|
||||
|
||||
App API
|
||||
-------
|
||||
|
||||
The App API is used for communication between the pretixLEAD app and the pretix server.
|
||||
|
||||
.. warning:: We consider this an internal API, it is not intended for external use. You may still use it, but
|
||||
our :ref:`compatibility commitment <rest-compat>` does not apply.
|
||||
|
||||
Authentication
|
||||
""""""""""""""
|
||||
|
||||
Every exhibitor has an "access code", usually consisting of 8 alphanumeric uppercase characters.
|
||||
This access code is communicated to event exhibitors by the event organizers, so this is also what
|
||||
exhibitors should enter into a login screen.
|
||||
|
||||
All API requests need to contain this access code as a header like this::
|
||||
|
||||
Authorization: Exhibitor ABCDE123
|
||||
|
||||
Exhibitor profile
|
||||
"""""""""""""""""
|
||||
|
||||
Upon login and in regular intervals after that, the API should fetch the exhibitors profile.
|
||||
This serves two purposes:
|
||||
|
||||
* Checking if the authorization code is actually valid
|
||||
|
||||
* Obtaining information that can be shown in the app
|
||||
|
||||
The resource consists of the following fields:
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
===================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
name string Exhibitor name
|
||||
booth string Booth number (or ``null``)
|
||||
event object Object describing the event
|
||||
├ name multi-lingual string Event name
|
||||
├ imprint_url string URL to legal notice page. If not ``null``, a button in the app should link to this page.
|
||||
├ privacy_url string URL to privacy notice page. If not ``null``, a button in the app should link to this page.
|
||||
├ help_url string URL to help page. If not ``null``, a button in the app should link to this page.
|
||||
├ logo_url string URL to event logo. If not ``null``, this logo may be shown in the app.
|
||||
├ slug string Event short form
|
||||
└ organizer string Organizer short form
|
||||
notes boolean Specifies whether the exhibitor is allowed to take notes on leads
|
||||
tags list of strings List of tags the exhibitor can assign to their leads
|
||||
scan_types list of objects Only used for a special case, fixed value that external API consumers should ignore
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. http:get:: /exhibitors/api/v1/profile
|
||||
|
||||
**Example request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /exhibitors/api/v1/profile HTTP/1.1
|
||||
Authorization: Exhibitor ABCDE123
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "Aperture Science",
|
||||
"booth": "A2",
|
||||
"event": {
|
||||
"name": {"en": "Sample conference", "de": "Beispielkonferenz"},
|
||||
"slug": "bigevents",
|
||||
"imprint_url": null,
|
||||
"privacy_url": null,
|
||||
"help_url": null,
|
||||
"logo_url": null,
|
||||
"organizer": "sampleconf"
|
||||
},
|
||||
"notes": true,
|
||||
"tags": ["foo", "bar"],
|
||||
"scan_types": [
|
||||
{
|
||||
"key": "lead",
|
||||
"label": "Lead Scanning"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Invalid authentication code
|
||||
|
||||
Submitting a lead
|
||||
"""""""""""""""""
|
||||
|
||||
After a ticket/badge is scanned, it should immediately be submitted to the server
|
||||
so the scan is stored and information about the person can be shown in the app. The same
|
||||
code can be submitted multiple times, so it's no problem to just submit it again after the
|
||||
exhibitor set a note or a rating (0-5) inside the app.
|
||||
|
||||
On the request, you should set the following properties:
|
||||
|
||||
* ``code`` with the scanned barcode
|
||||
* ``notes`` with the exhibitor's notes
|
||||
* ``scanned`` with the date and time of the actual scan (not the time of the upload)
|
||||
* ``scan_type`` set to ``lead`` statically
|
||||
* ``tags`` with the list of selected tags
|
||||
* ``rating`` with the rating assigned by the exhibitor
|
||||
* ``device_name`` with a user-specified name of the device used for scanning (max. 190 characters), or ``null``
|
||||
|
||||
If you submit ``tags`` and ``rating`` to be ``null`` and ``notes`` to be ``""``, the server
|
||||
responds with the previously saved information and will not delete that information. If you
|
||||
supply other values, the information saved on the server will be overridden.
|
||||
|
||||
The response will also contain ``tags``, ``rating``, and ``notes``. Additionally,
|
||||
it will include ``attendee`` with a list of ``fields`` that can be shown to the
|
||||
user. Each field has an internal ``id``, a human-readable ``label``, and a ``value`` (all strings).
|
||||
|
||||
Note that the ``fields`` array can contain any number of dynamic keys!
|
||||
Depending on the exhibitors permission and event configuration this might be empty,
|
||||
or contain lots of details. The app should dynamically show these values (read-only)
|
||||
with the labels sent by the server.
|
||||
|
||||
The request for this looks like this:
|
||||
|
||||
.. http:post:: /exhibitors/api/v1/leads/
|
||||
|
||||
**Example request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /exhibitors/api/v1/leads/ HTTP/1.1
|
||||
Authorization: Exhibitor ABCDE123
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"code": "qrcodecontent",
|
||||
"notes": "Great customer, wants our newsletter",
|
||||
"scanned": "2020-10-18T12:24:23.000+00:00",
|
||||
"scan_type": "lead",
|
||||
"tags": ["foo"],
|
||||
"rating": 4,
|
||||
"device_name": "DEV1"
|
||||
}
|
||||
|
||||
**Example response:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"attendee": {
|
||||
"fields": [
|
||||
{
|
||||
"id": "attendee_name",
|
||||
"label": "Name",
|
||||
"value": "Jon Doe"
|
||||
},
|
||||
{
|
||||
"id": "attendee_email",
|
||||
"label": "Email",
|
||||
"value": "test@example.com"
|
||||
}
|
||||
]
|
||||
},
|
||||
"rating": 4,
|
||||
"tags": ["foo"],
|
||||
"notes": "Great customer, wants our newsletter"
|
||||
}
|
||||
|
||||
:statuscode 200: No error, leads was not scanned for the first time
|
||||
:statuscode 201: No error, leads was scanned for the first time
|
||||
:statuscode 400: Invalid data submitted
|
||||
:statuscode 401: Invalid authentication code
|
||||
301
doc/plugins/imported_secrets.rst
Normal file
301
doc/plugins/imported_secrets.rst
Normal file
@@ -0,0 +1,301 @@
|
||||
Secrets Import
|
||||
==============
|
||||
|
||||
Usually, pretix generates ticket secrets (i.e. the QR code used for scanning) itself. You can read more about this
|
||||
process at :ref:`secret_generators`.
|
||||
|
||||
With the "Secrets Import" plugin, you can upload your own list of secrets to be used instead. This is useful for
|
||||
integrating with third-party check-in systems.
|
||||
|
||||
|
||||
API Resource description
|
||||
-------------------------
|
||||
|
||||
The secrets import plugin provides a HTTP API that allows you to create new secrets.
|
||||
|
||||
The imported secret resource contains the following public fields:
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
===================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal ID of the secret
|
||||
secret string Actual string content of the secret (QR code content)
|
||||
used boolean Whether the secret was already used for a ticket. If ``true``,
|
||||
the secret can no longer be deleted. Secrets are never used
|
||||
twice, even if an order is canceled or deleted.
|
||||
item integer Internal ID of a product, or ``null``. If set, the secret
|
||||
will only be used for tickets of this product.
|
||||
variation integer Internal ID of a product variation, or ``null``. If set, the secret
|
||||
will only be used for tickets of this product variation.
|
||||
subevent integer Internal ID of an event series date, or ``null``. If set, the secret
|
||||
will only be used for tickets of this event series date.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
API Endpoints
|
||||
-------------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/imported_secrets/
|
||||
|
||||
Returns a list of all secrets imported for an event.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/imported_secrets/ 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,
|
||||
"secret": "foobar",
|
||||
"used": false,
|
||||
"item": null,
|
||||
"variation": null,
|
||||
"subevent": null
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query page: The page number in case of a multi-page result set, default is 1
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to view it.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/imported_secrets/(id)/
|
||||
|
||||
Returns information on one secret, identified by its ID.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/imported_secrets/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,
|
||||
"secret": "foobar",
|
||||
"used": false,
|
||||
"item": null,
|
||||
"variation": null,
|
||||
"subevent": null
|
||||
}
|
||||
|
||||
: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 secret to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event/secret does not exist **or** you have no permission to view it.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/imported_secrets/
|
||||
|
||||
Create a new secret.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/imported_secrets/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 166
|
||||
|
||||
{
|
||||
"secret": "foobar",
|
||||
"used": false,
|
||||
"item": null,
|
||||
"variation": null,
|
||||
"subevent": null
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"secret": "foobar",
|
||||
"used": false,
|
||||
"item": null,
|
||||
"variation": null,
|
||||
"subevent": null
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to a create new secret for
|
||||
:param event: The ``slug`` field of the event to create a new secret for
|
||||
:statuscode 201: no error
|
||||
:statuscode 400: The secret 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 secrets.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/imported_secrets/bulk_create/
|
||||
|
||||
Create new secrets in bulk (up to 500 per request). The request either succeeds or fails entirely.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/imported_secrets/bulk_create/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 166
|
||||
|
||||
[
|
||||
{
|
||||
"secret": "foobar",
|
||||
"used": false,
|
||||
"item": null,
|
||||
"variation": null,
|
||||
"subevent": null
|
||||
},
|
||||
{
|
||||
"secret": "baz",
|
||||
"used": false,
|
||||
"item": null,
|
||||
"variation": null,
|
||||
"subevent": null
|
||||
}
|
||||
]
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"secret": "foobar",
|
||||
"used": false,
|
||||
"item": null,
|
||||
"variation": null,
|
||||
"subevent": null
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"secret": "baz",
|
||||
"used": false,
|
||||
"item": null,
|
||||
"variation": null,
|
||||
"subevent": null
|
||||
}
|
||||
]
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to create new secrets for
|
||||
:param event: The ``slug`` field of the event to create new secrets for
|
||||
:statuscode 201: no error
|
||||
:statuscode 400: The secrets 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 secrets.
|
||||
|
||||
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/imported_secrets/(id)/
|
||||
|
||||
Update a secret. 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/imported_secrets/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 34
|
||||
|
||||
{
|
||||
"item": 2
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"secret": "foobar",
|
||||
"used": false,
|
||||
"item": 2,
|
||||
"variation": null,
|
||||
"subevent": null
|
||||
}
|
||||
|
||||
: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 secret to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The secret could not be modified due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event/secret does not exist **or** you have no permission to change it.
|
||||
|
||||
|
||||
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/imported_secrets/(id)/
|
||||
|
||||
Delete a secret. You can only delete secrets that have not yet been used.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v1/organizers/bigevents/events/sampleconf/imported_secrets/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 secret to delete
|
||||
:statuscode 204: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event/secret does not exist **or** you have no permission to change it **or** the secret has already been used
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
.. _`plugin-docs`:
|
||||
|
||||
Plugin documentation
|
||||
====================
|
||||
|
||||
@@ -10,10 +12,13 @@ If you want to **create** a plugin, please go to the
|
||||
:maxdepth: 2
|
||||
|
||||
list
|
||||
pretixdroid
|
||||
banktransfer
|
||||
ticketoutputpdf
|
||||
badges
|
||||
campaigns
|
||||
certificates
|
||||
digital
|
||||
exhibitors
|
||||
imported_secrets
|
||||
webinar
|
||||
presale-saml
|
||||
|
||||
@@ -5,6 +5,6 @@ List of plugins
|
||||
===============
|
||||
|
||||
A detailed list of plugins that are available for pretix can be found on the
|
||||
`project website`_.
|
||||
`pretix Marketplace`_.
|
||||
|
||||
.. _project website: https://pretix.eu/about/en/plugins
|
||||
.. _pretix Marketplace: https://marketplace.pretix.eu
|
||||
|
||||
405
doc/plugins/presale-saml.rst
Normal file
405
doc/plugins/presale-saml.rst
Normal file
@@ -0,0 +1,405 @@
|
||||
.. highlight:: ini
|
||||
.. spelling::
|
||||
|
||||
IdP
|
||||
skIDentity
|
||||
ePA
|
||||
NPA
|
||||
|
||||
Presale SAML Authentication
|
||||
===========================
|
||||
|
||||
The Presale SAML Authentication plugin is an advanced plugin, which most event
|
||||
organizers will not need to use. However, for the select few who do require
|
||||
strong customer authentication that cannot be covered by the built-in customer
|
||||
account functionality, this plugin allows pretix to connect to a SAML IdP and
|
||||
perform authentication and retrieval of user information.
|
||||
|
||||
Usage of the plugin is governed by two separate sets of settings: The plugin
|
||||
installation, the Service Provider (SP) configuration and the event
|
||||
configuration.
|
||||
|
||||
Plugin installation and initial configuration
|
||||
---------------------------------------------
|
||||
|
||||
.. note:: If you are a customer of our hosted `pretix.eu`_ offering, you can
|
||||
skip this section.
|
||||
|
||||
The plugin is installed as any other plugin in the pretix ecosystem. As a
|
||||
pretix system administrator, please follow the instructions in the the
|
||||
:ref:`Administrator documentation <admindocs>`.
|
||||
|
||||
Once installed, you will need to assess, if you want (or need) your pretix
|
||||
instance to be a single SP for all organizers and events or if every event
|
||||
organizer has to provide their own SP.
|
||||
|
||||
Take the example of a university which runs pretix under an pretix Enterprise
|
||||
agreement. Since they only provide ticketing services to themselves (every
|
||||
organizer is still just a different department of the same university), a
|
||||
single SP should be enough.
|
||||
|
||||
On the other hand, a reseller such as `pretix.eu`_ who services a multitude
|
||||
of clients would not work that way. Here, every organizer is a separate
|
||||
legal entity and as such will also need to provide their own SP configuration:
|
||||
Company A will expect their SP to reflect their company - and not a generalized
|
||||
"pretix SP".
|
||||
|
||||
Once you have decided on the mode of operation, the :ref:`Configuration file
|
||||
<config>` needs to be extended to reflect your choice.
|
||||
|
||||
Example::
|
||||
|
||||
[presale-saml]
|
||||
level=global
|
||||
|
||||
``level``
|
||||
``global`` to use only a single, system-wide SP, ``organizer`` for multiple
|
||||
SPs, configured on the organizer-level. Defaults to ``organizer``.
|
||||
|
||||
Service Provider configuration
|
||||
------------------------------
|
||||
|
||||
Global Level
|
||||
^^^^^^^^^^^^
|
||||
|
||||
.. note:: If you are a customer of our hosted `pretix.eu`_ offering, you can
|
||||
skip this section and follow the instructions on the upcoming
|
||||
Organizer Level settings.
|
||||
|
||||
As a user with administrative privileges, please activate them by clicking the
|
||||
`Admin Mode` button in the top right hand corner.
|
||||
|
||||
You should now see a new menu-item titled `SAML` appear.
|
||||
|
||||
Organizer Level
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
Navigate to the organizer settings in the pretix backend. In the navigation
|
||||
bar, you will find a menu-item titled `SAML` if your user has the `Can
|
||||
change organizer settings` permission.
|
||||
|
||||
|
||||
.. note:: If you are a customer of our hosted `pretix.eu`_ offering, the menu
|
||||
will only appear once one of our friendly customer service agents
|
||||
has enabled the Presale SAML Authentication plugin for at least one
|
||||
of your events. Feel free to get in touch with us!
|
||||
|
||||
Setting up the SP
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
No matter where your SP configuration lives, you will be greeted by a very
|
||||
long list of fields of which almost all of them will need to be filled. Please
|
||||
don't be discouraged - most of the settings don't need to be decided by yourself
|
||||
and/or are already preset with a sensible default setting.
|
||||
|
||||
If you are not sure what setting you should choose for any of the fields, you
|
||||
should reach out to your IdP operator as they can tell you exactly what the IdP
|
||||
expects and - more importantly - supports.
|
||||
|
||||
``IdP Metadata URL``
|
||||
Please provide the URL where your IdP outputs its metadata. For most IdPs,
|
||||
this URL is static and the same for all SPs. If you are a member of the
|
||||
DFN-AAI, you can find the meta-data for the `Test-, Basic- and
|
||||
Advanced-Federation`_ on their website. Please do talk with your local
|
||||
IdP operator though, as you might not even need to go through the DFN-AAI
|
||||
and might just use your institutions local IdP which will also host their
|
||||
metadata on a different URL.
|
||||
|
||||
The URL needs to be publicly accessible, as saving the settings form will
|
||||
fail if the IdP metadata cannot be retrieved. pretix will also automatically
|
||||
refresh the IdP metadata on a regular basis.
|
||||
|
||||
``SP Entity Id``
|
||||
By default, we recommend that you use the system-proposed metadata-URL as
|
||||
the Entity Id of your SP. However, if so desired or required by your IdP,
|
||||
you can also set any other, arbitrary URL as the SP Entity Id.
|
||||
|
||||
``SP Name / SP Decription``
|
||||
Most IdP will display the name and description of your SP to the users
|
||||
during authentication. The description field can be used to explain to the
|
||||
users how their data is being used.
|
||||
|
||||
``SP X.509 Certificate / SP X.509 Private Key``
|
||||
Your SP needs a certificate and a private key for said certificate. Please
|
||||
coordinate with your IdP, if you are supposed to generate these yourself or
|
||||
if they are provided to you.
|
||||
|
||||
``SP X.509 New Certificate``
|
||||
As certificates have an expiry date, they need to be renewed on a regular
|
||||
basis. In order to facilitate the rollover from the expiring to the new
|
||||
certificate, you can provide the new certificate already before the expiration
|
||||
of the existing one. That way, the system will automatically use the correct
|
||||
one. Once the old certificate has expired and is not used anymore at all,
|
||||
you can move the new certificate into the slot of the normal certificate and
|
||||
keep the new slot empty for your next renewal process.
|
||||
|
||||
``Requested Attributes``
|
||||
An IdP can hold a variety of attributes of an authenticating user. While
|
||||
your IdP will dictate which of the available attributes your SP can consume
|
||||
in theory, you will still need to define exactly which attributes the SP
|
||||
should request.
|
||||
|
||||
The notation is a JSON list of objects with 5 attributes each:
|
||||
|
||||
* ``attributeValue``: Can be defaulted to ``[]``.
|
||||
* ``friendlyName``: String used in the upcoming event-level settings to
|
||||
retrieve the attributes data.
|
||||
* ``isRequired``: Boolean indicating whether the IdP must enforce the
|
||||
transmission of this attribute. In most cases, ``true`` is the best
|
||||
choice.
|
||||
* ``name``: String of the internal, technical name of the requested
|
||||
attribute. Often starting with ``urn:mace:dir:attribute-def:``,
|
||||
``urn:oid:`` or ``http://``/``https://``.
|
||||
* ``nameFormat``: String describing the type of ``name`` that has been
|
||||
set in the previous section. Often starting with
|
||||
``urn:mace:shibboleth:1.0:`` or ``urn:oasis:names:tc:SAML:2.0:``.
|
||||
|
||||
Your IdP can provide you with a list of available attributes. See below
|
||||
for a sample configuration in an academic context.
|
||||
|
||||
Note, that you can have multiple attributes with the same ``friendlyName``
|
||||
but different ``name``s. This is often used in systems, where the same
|
||||
information (for example a persons name) is saved in different fields -
|
||||
for example because one institution is returning SAML 1.0 and other
|
||||
institutions are returning SAML 2.0 style attributes. Typically, this only
|
||||
occurs in mix environments like the DFN-AAI with a large number of
|
||||
participants. If you are only using your own institutions IdP and not
|
||||
authenticating anyone outside of your realm, this should not be a common
|
||||
sight.
|
||||
|
||||
``Encrypt/Sign/Require ...``
|
||||
Does what is says on the box - please inquire with your IdP for the
|
||||
necessary settings. Most settings can be turned on as they increase security,
|
||||
however some IdPs might stumble over some of them.
|
||||
|
||||
``Signature / Digest Algorithm``
|
||||
Please chose appropriate algorithms, that both pretix/your SP and the IdP
|
||||
can communicate with. A common source of issues when connecting to a
|
||||
Shibboleth-based IdP is the Digest Algorithm: pretix does not support
|
||||
``http://www.w3.org/2009/xmlenc11#rsa-oaep`` and authentication will fail
|
||||
if the IdP enforces this.
|
||||
|
||||
``Technical/Support Contacts``
|
||||
Those contacts are encoded into the SPs public meta data and might be
|
||||
displayed to users having trouble authenticating. It is recommended to
|
||||
provide a dedicated point of contact for technical issues, as those will
|
||||
be the ones to change the configuration for the SP.
|
||||
|
||||
Event / Authentication configuration
|
||||
------------------------------------
|
||||
|
||||
Basic settings
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
Once the plugin has been enabled for a pretix event using the Plugins-menu from
|
||||
the event's settings, a new *SAML* menu item will show up.
|
||||
|
||||
On this page, the actual authentication can be configured.
|
||||
|
||||
``Checkout Explanation``
|
||||
Since most users probably won't be familiar with why they have to authenticate
|
||||
to buy a ticket, you can provide them a small blurb here. Markdown is supported.
|
||||
|
||||
``Attribute RegEx``
|
||||
By default, any successful authentication with the IdP will allow the user to
|
||||
proceed with their purchase. Should the allowed audience needed to be restricted
|
||||
further, a set of regular Expressions can be used to do this.
|
||||
|
||||
An Attribute RegEx of ``{}`` will allow any authenticated user to pass.
|
||||
|
||||
A RegEx of ``{ "affiliation": "^(employee@pretix.eu|staff@pretix.eu)$" }`` will
|
||||
only allow user to pass which have the ``affiliation`` attribute and whose
|
||||
attribute either matches ``employee@pretix.eu`` or ``staff@pretix.eu``.
|
||||
|
||||
Please make sure that the attribute you are querying is also requested from the
|
||||
IdP in the first place - for a quick check you can have a look at the top of
|
||||
the page where all currently configured attributes are listed.
|
||||
|
||||
``RegEx Fail Explanation``
|
||||
Only used in conjunction with the above Attribute RegEx. Should the user not
|
||||
pass the restrictions imposed by the regular expression, the user is shown
|
||||
this error-message.
|
||||
|
||||
If you are - for example in an university context - restricting access to
|
||||
students only, you might want to explain here that Employees are not allowed
|
||||
to book tickets.
|
||||
|
||||
``Ticket Secret SAML Attribute``
|
||||
In very specific instances, it might be desirable that the ticket-secret is
|
||||
not the randomly one generated by pretix but rather based on one of the
|
||||
users attributes - for example their unique ID or access card number.
|
||||
|
||||
To achieve this, the name of a SAML-attribute can be specified here.
|
||||
|
||||
It is however necessary to note, that even with this setting in use,
|
||||
ticket-secrets need to be unique. This is why when this setting is enabled,
|
||||
the default, pretix-generated ticket-secret is prefixed with the attributes
|
||||
value.
|
||||
|
||||
Example: A users ``cardid`` attribute has the value of ``01189998819991197253``.
|
||||
The default random ticket secret would have been
|
||||
``yczygpw9877akz2xwdhtdyvdqwkv7npj``. The resulting new secret will now be
|
||||
``01189998819991197253_yczygpw9877akz2xwdhtdyvdqwkv7npj``.
|
||||
|
||||
That way, the ticket secret is still unique, but when checking into an event,
|
||||
the user can easily be searched and found using their identifier.
|
||||
|
||||
IdP-provided E-Mail addresses, names
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
By default, pretix will only authenticate the user and not process the received
|
||||
data any further.
|
||||
|
||||
However, there are a few exceptions to this rule.
|
||||
|
||||
There are a few `magic` attributes that pretix will use to automatically populate
|
||||
the corresponding fields within the checkout process **and lock them out from
|
||||
user editing**.
|
||||
|
||||
* ``givenName`` and ``sn``: If both of those attributes are present and pretix
|
||||
is configured to collect the users name, these attributes' values are used
|
||||
for the given and family name respectively.
|
||||
* ``email``: If this attribute is present, the E-Mail-address of the users will
|
||||
be set to the one transmitted through the attributes.
|
||||
|
||||
The latter might pose a problem, if the IdP is transmitting an ``email`` attribute
|
||||
which does contain a system-level mail address which is only used as an internal
|
||||
identifier but not as a real mailbox. In this case, please consider setting the
|
||||
``friendlyName`` of the attribute to a different value than ``email`` or removing
|
||||
this field from the list of requested attributes altogether.
|
||||
|
||||
Saving attributes to questions
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
By setting the ``internal identifier`` of a user-defined question to the same name
|
||||
as a SAML attribute, pretix will save the value of said attribute into the question.
|
||||
|
||||
All the same as in the above section on E-Mail addresses, those fields become
|
||||
non-editable by the user.
|
||||
|
||||
Please be aware that some specialty question types might not be compatible with
|
||||
the SAML attributes due to specific format requirements. If in doubt (or if the
|
||||
checkout fails/the information is not properly saved), try setting the question
|
||||
type to a simple type like "Text (one line)".
|
||||
|
||||
Notes and configuration examples
|
||||
--------------------------------
|
||||
|
||||
Requesting SAML 1.0 and 2.0 attributes from an academic IdP
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
This requests the ``eduPersonPrincipalName`` (also sometimes called EPPN),
|
||||
``email``, ``givenName`` and ``sn`` both in SAML 1.0 and SAML 2.0 attributes.
|
||||
|
||||
.. sourcecode:: json
|
||||
|
||||
[
|
||||
{
|
||||
"attributeValue": [],
|
||||
"friendlyName": "eduPersonPrincipalName",
|
||||
"isRequired": true,
|
||||
"name": "urn:mace:dir:attribute-def:eduPersonPrincipalName",
|
||||
"nameFormat": "urn:mace:shibboleth:1.0:attributeNamespace:uri"
|
||||
},
|
||||
{
|
||||
"attributeValue": [],
|
||||
"friendlyName": "eduPersonPrincipalName",
|
||||
"isRequired": true,
|
||||
"name": "urn:oid:1.3.6.1.4.1.5923.1.1.1.6",
|
||||
"nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
|
||||
},
|
||||
{
|
||||
"attributeValue": [],
|
||||
"friendlyName": "email",
|
||||
"isRequired": true,
|
||||
"name": "urn:mace:dir:attribute-def:mail",
|
||||
"nameFormat": "urn:mace:shibboleth:1.0:attributeNamespace:uri"
|
||||
},
|
||||
{
|
||||
"attributeValue": [],
|
||||
"friendlyName": "email",
|
||||
"isRequired": true,
|
||||
"name": "urn:oid:0.9.2342.19200300.100.1.3",
|
||||
"nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
|
||||
},
|
||||
{
|
||||
"attributeValue": [],
|
||||
"friendlyName": "givenName",
|
||||
"isRequired": true,
|
||||
"name": "urn:mace:dir:attribute-def:givenName",
|
||||
"nameFormat": "urn:mace:shibboleth:1.0:attributeNamespace:uri"
|
||||
},
|
||||
{
|
||||
"attributeValue": [],
|
||||
"friendlyName": "givenName",
|
||||
"isRequired": true,
|
||||
"name": "urn:oid:2.5.4.42",
|
||||
"nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
|
||||
},
|
||||
{
|
||||
"attributeValue": [],
|
||||
"friendlyName": "sn",
|
||||
"isRequired": true,
|
||||
"name": "urn:mace:dir:attribute-def:sn",
|
||||
"nameFormat": "urn:mace:shibboleth:1.0:attributeNamespace:uri"
|
||||
},
|
||||
{
|
||||
"attributeValue": [],
|
||||
"friendlyName": "sn",
|
||||
"isRequired": true,
|
||||
"name": "urn:oid:2.5.4.4",
|
||||
"nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
|
||||
}
|
||||
]
|
||||
|
||||
skIDentity IdP Metadata URL
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Since the IdP Metadata URL for `skIDentity`_ is not readily documented/visible
|
||||
in their backend, we document it here:
|
||||
``https://service.skidentity.de/fs/saml/metadata``
|
||||
|
||||
Requesting skIDentity attributes for electronic identity cards
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
This requests the basic ``eIdentifier``, ``IDType``, ``IDIssuer``, and
|
||||
``NameID`` from the `skIDentity`_ SAML service, which are available for
|
||||
electronic ID cards such as the German ePA/NPA. (Other attributes such as
|
||||
the name and address are available at additional cost from the IdP).
|
||||
|
||||
.. sourcecode:: json
|
||||
|
||||
[
|
||||
{
|
||||
"attributeValue": [],
|
||||
"friendlyName": "eIdentifier",
|
||||
"isRequired": true,
|
||||
"name": "http://www.skidentity.de/att/eIdentifier",
|
||||
"nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
|
||||
},
|
||||
{
|
||||
"attributeValue": [],
|
||||
"friendlyName": "IDType",
|
||||
"isRequired": true,
|
||||
"name": "http://www.skidentity.de/att/IDType",
|
||||
"nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
|
||||
},
|
||||
{
|
||||
"attributeValue": [],
|
||||
"friendlyName": "IDIssuer",
|
||||
"isRequired": true,
|
||||
"name": "http://www.skidentity.de/att/IDIssuer",
|
||||
"nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
|
||||
},
|
||||
{
|
||||
"attributeValue": [],
|
||||
"friendlyName": "NameID",
|
||||
"isRequired": true,
|
||||
"name": "http://www.skidentity.de/att/NameID",
|
||||
"nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
|
||||
}
|
||||
]
|
||||
|
||||
.. _pretix.eu: https://pretix.eu
|
||||
.. _Test-, Basic- and Advanced-Federation: https://doku.tid.dfn.de/en:metadata
|
||||
.. _skIDentity: https://www.skidentity.de/
|
||||
@@ -1,368 +0,0 @@
|
||||
pretixdroid HTTP API
|
||||
====================
|
||||
|
||||
The pretixdroid plugin provides a HTTP API that the `pretixdroid Android app`_
|
||||
uses to communicate with the pretix server.
|
||||
|
||||
.. warning:: This API is **DEPRECATED** and will probably go away soon. It is used **only** to serve the pretixdroid
|
||||
Android app. There are no backwards compatibility guarantees on this API. We will not add features that
|
||||
are not required for the Android App. There is a general-purpose :ref:`rest-api` that provides all
|
||||
features that you need to check in.
|
||||
|
||||
.. versionchanged:: 1.12
|
||||
|
||||
Support for check-in-time questions has been added. The new API features are fully backwards-compatible and
|
||||
negotiated live, so clients which do not need this feature can ignore the change. For this reason, the API version
|
||||
has not been increased and is still set to 3.
|
||||
|
||||
.. versionchanged:: 1.13
|
||||
|
||||
Support for checking in unpaid tickets has been added.
|
||||
|
||||
|
||||
.. http:post:: /pretixdroid/api/(organizer)/(event)/redeem/
|
||||
|
||||
Redeems a ticket, i.e. checks the user in.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /pretixdroid/api/demoorga/democon/redeem/?key=ABCDEF HTTP/1.1
|
||||
Host: demo.pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
secret=az9u4mymhqktrbupmwkvv6xmgds5dk3&questions_supported=true
|
||||
|
||||
You **must** set the parameter secret.
|
||||
|
||||
You **must** set the parameter ``questions_supported`` to ``true`` **if** you support asking questions
|
||||
back to the app operator. You **must not** set it if you do not support this feature. In that case, questions
|
||||
will just be ignored.
|
||||
|
||||
You **may** set the additional parameter ``datetime`` in the body containing an ISO8601-encoded
|
||||
datetime of the entry attempt. If you don"t, the current date and time will be used.
|
||||
|
||||
You **may** set the additional parameter ``force`` to indicate that the request should be logged
|
||||
regardless of previous check-ins for the same ticket. This might be useful if you made the entry decision offline.
|
||||
Questions will also always be ignored in this case (i.e. supplied answers will be saved, but no error will be
|
||||
thrown if they are missing or invalid).
|
||||
|
||||
You **may** set the additional parameter ``nonce`` with a globally unique random value to identify this
|
||||
check-in. This is meant to be used to prevent duplicate check-ins when you are just retrying after a connection
|
||||
failure.
|
||||
|
||||
You **may** set the additional parameter ``ignore_unpaid`` to indicate that the check-in should be performed even
|
||||
if the order is in pending state.
|
||||
|
||||
If questions are supported and required, you will receive a dictionary ``questions`` containing details on the
|
||||
particular questions to ask. To answer them, just re-send your redemption request with additional parameters of
|
||||
the form ``answer_<question>=<answer>``, e.g. ``answer_12=24``.
|
||||
|
||||
**Example successful response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/json
|
||||
|
||||
{
|
||||
"status": "ok"
|
||||
"version": 3,
|
||||
"data": {
|
||||
"secret": "az9u4mymhqktrbupmwkvv6xmgds5dk3",
|
||||
"order": "ABCDE",
|
||||
"item": "Standard ticket",
|
||||
"item_id": 1,
|
||||
"variation": null,
|
||||
"variation_id": null,
|
||||
"attendee_name": "Peter Higgs",
|
||||
"attention": false,
|
||||
"redeemed": true,
|
||||
"checkin_allowed": true,
|
||||
"addons_text": "Parking spot",
|
||||
"paid": true
|
||||
}
|
||||
}
|
||||
|
||||
**Example response with required questions**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/json
|
||||
|
||||
{
|
||||
"status": "incomplete"
|
||||
"version": 3
|
||||
"data": {
|
||||
"secret": "az9u4mymhqktrbupmwkvv6xmgds5dk3",
|
||||
"order": "ABCDE",
|
||||
"item": "Standard ticket",
|
||||
"item_id": 1,
|
||||
"variation": null,
|
||||
"variation_id": null,
|
||||
"attendee_name": "Peter Higgs",
|
||||
"attention": false,
|
||||
"redeemed": true,
|
||||
"checkin_allowed": true,
|
||||
"addons_text": "Parking spot",
|
||||
"paid": true
|
||||
},
|
||||
"questions": [
|
||||
{
|
||||
"id": 12,
|
||||
"type": "C",
|
||||
"question": "Choose a shirt size",
|
||||
"required": true,
|
||||
"position": 2,
|
||||
"items": [1],
|
||||
"options": [
|
||||
{
|
||||
"id": 24,
|
||||
"answer": "M"
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"answer": "L"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
**Example error response with data**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/json
|
||||
|
||||
{
|
||||
"status": "error",
|
||||
"reason": "already_redeemed",
|
||||
"version": 3,
|
||||
"data": {
|
||||
"secret": "az9u4mymhqktrbupmwkvv6xmgds5dk3",
|
||||
"order": "ABCDE",
|
||||
"item": "Standard ticket",
|
||||
"item_id": 1,
|
||||
"variation": null,
|
||||
"variation_id": null,
|
||||
"attendee_name": "Peter Higgs",
|
||||
"attention": false,
|
||||
"redeemed": true,
|
||||
"checkin_allowed": true,
|
||||
"addons_text": "Parking spot",
|
||||
"paid": true
|
||||
}
|
||||
}
|
||||
|
||||
**Example error response without data**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/json
|
||||
|
||||
{
|
||||
"status": "error",
|
||||
"reason": "unkown_ticket",
|
||||
"version": 3
|
||||
}
|
||||
|
||||
Possible error reasons:
|
||||
|
||||
* ``unpaid`` - Ticket is not paid for or has been refunded
|
||||
* ``already_redeemed`` - Ticket already has been redeemed
|
||||
* ``product`` - Tickets with this product may not be scanned at this device
|
||||
* ``unknown_ticket`` - Secret does not match a ticket in the database
|
||||
|
||||
:query key: Secret API key
|
||||
:statuscode 200: Valid request
|
||||
:statuscode 404: Unknown organizer or event
|
||||
:statuscode 403: Invalid authorization key
|
||||
|
||||
.. http:get:: /pretixdroid/api/(organizer)/(event)/search/
|
||||
|
||||
Searches for a ticket.
|
||||
At most 25 results will be returned. **Queries with less than 4 characters will always return an empty result set.**
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /pretixdroid/api/demoorga/democon/search/?key=ABCDEF&query=Peter HTTP/1.1
|
||||
Host: demo.pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/json
|
||||
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"secret": "az9u4mymhqktrbupmwkvv6xmgds5dk3",
|
||||
"order": "ABCE6",
|
||||
"item": "Standard ticket",
|
||||
"variation": null,
|
||||
"attendee_name": "Peter Higgs",
|
||||
"redeemed": false,
|
||||
"attention": false,
|
||||
"checkin_allowed": true,
|
||||
"addons_text": "Parking spot",
|
||||
"paid": true
|
||||
},
|
||||
...
|
||||
],
|
||||
"version": 3
|
||||
}
|
||||
|
||||
:query query: Search query
|
||||
:query key: Secret API key
|
||||
:statuscode 200: Valid request
|
||||
:statuscode 404: Unknown organizer or event
|
||||
:statuscode 403: Invalid authorization key
|
||||
|
||||
.. http:get:: /pretixdroid/api/(organizer)/(event)/download/
|
||||
|
||||
Download data for all tickets.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /pretixdroid/api/demoorga/democon/download/?key=ABCDEF HTTP/1.1
|
||||
Host: demo.pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/json
|
||||
|
||||
{
|
||||
"version": 3,
|
||||
"results": [
|
||||
{
|
||||
"secret": "az9u4mymhqktrbupmwkvv6xmgds5dk3",
|
||||
"order": "ABCE6",
|
||||
"item": "Standard ticket",
|
||||
"variation": null,
|
||||
"attendee_name": "Peter Higgs",
|
||||
"redeemed": false,
|
||||
"attention": false,
|
||||
"checkin_allowed": true,
|
||||
"paid": true
|
||||
},
|
||||
...
|
||||
],
|
||||
"questions": [
|
||||
{
|
||||
"id": 12,
|
||||
"type": "C",
|
||||
"question": "Choose a shirt size",
|
||||
"required": true,
|
||||
"position": 2,
|
||||
"items": [1],
|
||||
"options": [
|
||||
{
|
||||
"id": 24,
|
||||
"answer": "M"
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"answer": "L"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query key: Secret API key
|
||||
:statuscode 200: Valid request
|
||||
:statuscode 404: Unknown organizer or event
|
||||
:statuscode 403: Invalid authorization key
|
||||
|
||||
.. http:get:: /pretixdroid/api/(organizer)/(event)/status/
|
||||
|
||||
Returns status information, such as the total number of tickets and the
|
||||
number of performed check-ins.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /pretixdroid/api/demoorga/democon/status/?key=ABCDEF HTTP/1.1
|
||||
Host: demo.pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/json
|
||||
|
||||
{
|
||||
"checkins": 17,
|
||||
"total": 42,
|
||||
"version": 3,
|
||||
"event": {
|
||||
"name": "Demo Conference",
|
||||
"slug": "democon",
|
||||
"date_from": "2016-12-27T17:00:00Z",
|
||||
"date_to": "2016-12-30T18:00:00Z",
|
||||
"timezone": "UTC",
|
||||
"url": "https://demo.pretix.eu/demoorga/democon/",
|
||||
"organizer": {
|
||||
"name": "Demo Organizer",
|
||||
"slug": "demoorga"
|
||||
},
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"name": "T-Shirt",
|
||||
"id": 1,
|
||||
"checkins": 1,
|
||||
"admission": False,
|
||||
"total": 1,
|
||||
"variations": [
|
||||
{
|
||||
"name": "Red",
|
||||
"id": 1,
|
||||
"checkins": 1,
|
||||
"total": 12
|
||||
},
|
||||
{
|
||||
"name": "Blue",
|
||||
"id": 2,
|
||||
"checkins": 4,
|
||||
"total": 8
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Ticket",
|
||||
"id": 2,
|
||||
"checkins": 15,
|
||||
"admission": True,
|
||||
"total": 22,
|
||||
"variations": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query key: Secret API key
|
||||
:statuscode 200: Valid request
|
||||
:statuscode 404: Unknown organizer or event
|
||||
:statuscode 403: Invalid authorization key
|
||||
|
||||
.. _pretixdroid Android app: https://github.com/pretix/pretixdroid
|
||||
@@ -1,5 +1,6 @@
|
||||
-r ../src/requirements.txt
|
||||
-e ../src/
|
||||
sphinx==2.3.*
|
||||
jinja2==3.0.*
|
||||
sphinx-rtd-theme
|
||||
sphinxcontrib-httpdomain
|
||||
sphinxcontrib-images
|
||||
|
||||
@@ -17,10 +17,12 @@ bic
|
||||
BIC
|
||||
boolean
|
||||
booleans
|
||||
bugfix
|
||||
cancelled
|
||||
casted
|
||||
Ceph
|
||||
checkbox
|
||||
checkins
|
||||
checksum
|
||||
config
|
||||
contenttypes
|
||||
@@ -76,6 +78,7 @@ mixin
|
||||
mixins
|
||||
multi
|
||||
multidomain
|
||||
multiplicator
|
||||
namespace
|
||||
namespaced
|
||||
namespaces
|
||||
@@ -100,6 +103,7 @@ prepending
|
||||
preprocessor
|
||||
presale
|
||||
pretix
|
||||
pretixLEAD
|
||||
pretixSCAN
|
||||
pretixdroid
|
||||
pretixPOS
|
||||
|
||||
@@ -203,4 +203,4 @@ Then, please contact support@pretix.eu and we will enable DKIM for your domain o
|
||||
|
||||
|
||||
.. _Sender Policy Framework: https://en.wikipedia.org/wiki/Sender_Policy_Framework
|
||||
.. _SPF specification: http://www.openspf.org/SPF_Record_Syntax
|
||||
.. _SPF specification: http://www.open-spf.org/SPF_Record_Syntax
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
.. _secret_generators:
|
||||
|
||||
Ticket secret generators
|
||||
========================
|
||||
|
||||
|
||||
@@ -4,8 +4,7 @@ Embeddable Widget
|
||||
=================
|
||||
|
||||
If you want to show your ticket shop on your event website or blog, you can use our JavaScript widget. This way,
|
||||
users will not need to leave your site to buy their ticket in most cases. The widget will still open a new tab
|
||||
for the checkout if the user is on a mobile device.
|
||||
users will not need to leave your site to buy their ticket in most cases.
|
||||
|
||||
To obtain the correct HTML code for embedding your event into your website, we recommend that you go to the "Widget"
|
||||
tab of your event's settings. You can specify some optional settings there (for example the language of the widget)
|
||||
@@ -310,6 +309,10 @@ Currently, the following attributes are understood by pretix itself:
|
||||
always be modified. Note that this is not a security feature and can easily be overridden by users, so do not rely
|
||||
on this for authentication.
|
||||
|
||||
* If ``data-consent="…"`` is given, the cookie consent mechanism will be initialized with consent for the given cookie
|
||||
providers. All other providers will be disabled, no consent dialog will be shown. This is useful if you already
|
||||
asked the user for consent and don't want them to be asked again. Example: ``data-consent="facebook,google_analytics"``
|
||||
|
||||
Any configured pretix plugins might understand more data fields. For example, if the appropriate plugins on pretix
|
||||
Hosted or pretix Enterprise are active, you can pass the following fields:
|
||||
|
||||
|
||||
2
src/.gitignore
vendored
2
src/.gitignore
vendored
@@ -9,4 +9,4 @@ dist/
|
||||
*.bak
|
||||
pretix/static/jsi18n/
|
||||
node_modules/
|
||||
|
||||
.eggs/
|
||||
|
||||
@@ -34,5 +34,7 @@ git push
|
||||
# Unlock Weblate
|
||||
for c in $COMPONENTS; do
|
||||
wlc unlock $c;
|
||||
done
|
||||
for c in $COMPONENTS; do
|
||||
wlc pull $c;
|
||||
done
|
||||
|
||||
@@ -19,4 +19,4 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
__version__ = "4.0.0"
|
||||
__version__ = "4.8.0.dev0"
|
||||
|
||||
@@ -59,6 +59,7 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'api-v1:badgeitem-list'),
|
||||
('GET', 'api-v1:checkinlist-list'),
|
||||
('GET', 'api-v1:checkinlist-status'),
|
||||
('POST', 'api-v1:checkinlist-failed_checkins'),
|
||||
('GET', 'api-v1:checkinlistpos-list'),
|
||||
('POST', 'api-v1:checkinlistpos-redeem'),
|
||||
('GET', 'api-v1:revokedsecrets-list'),
|
||||
@@ -69,9 +70,9 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
|
||||
)
|
||||
|
||||
|
||||
class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
|
||||
class PretixScanNoSyncNoSearchSecurityProfile(AllowListSecurityProfile):
|
||||
identifier = 'pretixscan_online_kiosk'
|
||||
verbose_name = _('pretixSCAN (kiosk mode, online only)')
|
||||
verbose_name = _('pretixSCAN (kiosk mode, no order sync, no search)')
|
||||
allowlist = (
|
||||
('GET', 'api-v1:version'),
|
||||
('GET', 'api-v1:device.eventselection'),
|
||||
@@ -89,6 +90,37 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'api-v1:badgeitem-list'),
|
||||
('GET', 'api-v1:checkinlist-list'),
|
||||
('GET', 'api-v1:checkinlist-status'),
|
||||
('POST', 'api-v1:checkinlist-failed_checkins'),
|
||||
('POST', 'api-v1:checkinlistpos-redeem'),
|
||||
('GET', 'api-v1:revokedsecrets-list'),
|
||||
('GET', 'api-v1:orderposition-pdf_image'),
|
||||
('GET', 'api-v1:event.settings'),
|
||||
('POST', 'api-v1:upload'),
|
||||
)
|
||||
|
||||
|
||||
class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
|
||||
identifier = 'pretixscan_online_noorders'
|
||||
verbose_name = _('pretixSCAN (online only, no order sync)')
|
||||
allowlist = (
|
||||
('GET', 'api-v1:version'),
|
||||
('GET', 'api-v1:device.eventselection'),
|
||||
('POST', 'api-v1:device.update'),
|
||||
('POST', 'api-v1:device.revoke'),
|
||||
('POST', 'api-v1:device.roll'),
|
||||
('GET', 'api-v1:event-list'),
|
||||
('GET', 'api-v1:event-detail'),
|
||||
('GET', 'api-v1:subevent-list'),
|
||||
('GET', 'api-v1:subevent-detail'),
|
||||
('GET', 'api-v1:itemcategory-list'),
|
||||
('GET', 'api-v1:item-list'),
|
||||
('GET', 'api-v1:question-list'),
|
||||
('GET', 'api-v1:badgelayout-list'),
|
||||
('GET', 'api-v1:badgeitem-list'),
|
||||
('GET', 'api-v1:checkinlist-list'),
|
||||
('GET', 'api-v1:checkinlist-status'),
|
||||
('POST', 'api-v1:checkinlist-failed_checkins'),
|
||||
('GET', 'api-v1:checkinlistpos-list'),
|
||||
('POST', 'api-v1:checkinlistpos-redeem'),
|
||||
('GET', 'api-v1:revokedsecrets-list'),
|
||||
('GET', 'api-v1:orderposition-pdf_image'),
|
||||
@@ -131,9 +163,12 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
|
||||
('POST', 'api-v1:orderrefund-list'),
|
||||
('POST', 'api-v1:orderrefund-done'),
|
||||
('POST', 'api-v1:cartposition-list'),
|
||||
('POST', 'api-v1:cartposition-bulk-create'),
|
||||
('GET', 'api-v1:checkinlist-list'),
|
||||
('POST', 'api-v1:checkinlistpos-redeem'),
|
||||
('POST', 'plugins:pretix_posbackend:order.posprintlog'),
|
||||
('POST', 'plugins:pretix_posbackend:order.poslock'),
|
||||
('DELETE', 'plugins:pretix_posbackend:order.poslock'),
|
||||
('DELETE', 'api-v1:cartposition-detail'),
|
||||
('GET', 'api-v1:giftcard-list'),
|
||||
('POST', 'api-v1:giftcard-transact'),
|
||||
@@ -141,8 +176,11 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
|
||||
('POST', 'plugins:pretix_posbackend:posreceipt-list'),
|
||||
('POST', 'plugins:pretix_posbackend:posclosing-list'),
|
||||
('POST', 'plugins:pretix_posbackend:posdebugdump-list'),
|
||||
('POST', 'plugins:pretix_posbackend:posdebuglogentry-list'),
|
||||
('POST', 'plugins:pretix_posbackend:posdebuglogentry-bulk-create'),
|
||||
('GET', 'plugins:pretix_posbackend:poscashier-list'),
|
||||
('POST', 'plugins:pretix_posbackend:stripeterminal.token'),
|
||||
('PUT', 'plugins:pretix_posbackend:file.upload'),
|
||||
('GET', 'api-v1:revokedsecrets-list'),
|
||||
('GET', 'api-v1:event.settings'),
|
||||
('GET', 'plugins:pretix_seating:event.event'),
|
||||
@@ -158,6 +196,7 @@ DEVICE_SECURITY_PROFILES = {
|
||||
FullAccessSecurityProfile,
|
||||
PretixScanSecurityProfile,
|
||||
PretixScanNoSyncSecurityProfile,
|
||||
PretixScanNoSyncNoSearchSecurityProfile,
|
||||
PretixPosSecurityProfile,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -49,7 +49,9 @@ class EventPermission(BasePermission):
|
||||
if not request.user.is_authenticated and not isinstance(request.auth, (Device, TeamAPIToken)):
|
||||
return False
|
||||
|
||||
if request.method not in SAFE_METHODS and hasattr(view, 'write_permission'):
|
||||
if hasattr(view, '_get_permission_name'):
|
||||
required_permission = getattr(view, '_get_permission_name')(request)
|
||||
elif request.method not in SAFE_METHODS and hasattr(view, 'write_permission'):
|
||||
required_permission = getattr(view, 'write_permission')
|
||||
elif hasattr(view, 'permission'):
|
||||
required_permission = getattr(view, 'permission')
|
||||
|
||||
18
src/pretix/api/migrations/0006_alter_webhook_target_url.py
Normal file
18
src/pretix/api/migrations/0006_alter_webhook_target_url.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.2 on 2021-07-05 07:56
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixapi', '0005_auto_20191028_1541'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='webhook',
|
||||
name='target_url',
|
||||
field=models.URLField(max_length=255),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.4 on 2021-09-15 11:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixapi', '0006_alter_webhook_target_url'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='webhookcall',
|
||||
name='target_url',
|
||||
field=models.URLField(max_length=255),
|
||||
),
|
||||
]
|
||||
@@ -95,7 +95,7 @@ class OAuthRefreshToken(AbstractRefreshToken):
|
||||
class WebHook(models.Model):
|
||||
organizer = models.ForeignKey('pretixbase.Organizer', on_delete=models.CASCADE, related_name='webhooks')
|
||||
enabled = models.BooleanField(default=True, verbose_name=_("Enable webhook"))
|
||||
target_url = models.URLField(verbose_name=_("Target URL"))
|
||||
target_url = models.URLField(verbose_name=_("Target URL"), max_length=255)
|
||||
all_events = models.BooleanField(default=True, verbose_name=_("All events (including newly created ones)"))
|
||||
limit_events = models.ManyToManyField('pretixbase.Event', verbose_name=_("Limit to events"), blank=True)
|
||||
|
||||
@@ -120,7 +120,7 @@ class WebHookEventListener(models.Model):
|
||||
class WebHookCall(models.Model):
|
||||
webhook = models.ForeignKey('WebHook', on_delete=models.CASCADE, related_name='calls')
|
||||
datetime = models.DateTimeField(auto_now_add=True)
|
||||
target_url = models.URLField()
|
||||
target_url = models.URLField(max_length=255)
|
||||
action_type = models.CharField(max_length=255)
|
||||
is_retry = models.BooleanField(default=False)
|
||||
execution_time = models.FloatField(null=True)
|
||||
|
||||
27
src/pretix/api/pagination.py
Normal file
27
src/pretix/api/pagination.py
Normal file
@@ -0,0 +1,27 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from rest_framework.pagination import PageNumberPagination
|
||||
|
||||
|
||||
class Pagination(PageNumberPagination):
|
||||
page_size_query_param = 'page_size'
|
||||
max_page_size = 50
|
||||
@@ -73,53 +73,61 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
minutes=self.context['event'].settings.get('reservation_time', as_type=int)
|
||||
)
|
||||
|
||||
with self.context['event'].lock():
|
||||
new_quotas = (validated_data.get('variation').quotas.filter(subevent=validated_data.get('subevent'))
|
||||
if validated_data.get('variation')
|
||||
else validated_data.get('item').quotas.filter(subevent=validated_data.get('subevent')))
|
||||
if len(new_quotas) == 0:
|
||||
new_quotas = (validated_data.get('variation').quotas.filter(subevent=validated_data.get('subevent'))
|
||||
if validated_data.get('variation')
|
||||
else validated_data.get('item').quotas.filter(subevent=validated_data.get('subevent')))
|
||||
if len(new_quotas) == 0:
|
||||
raise ValidationError(
|
||||
gettext_lazy('The product "{}" is not assigned to a quota.').format(
|
||||
str(validated_data.get('item'))
|
||||
)
|
||||
)
|
||||
for quota in new_quotas:
|
||||
avail = quota.availability(_cache=self.context['quota_cache'])
|
||||
if avail[0] != Quota.AVAILABILITY_OK or (avail[1] is not None and avail[1] < 1):
|
||||
raise ValidationError(
|
||||
gettext_lazy('The product "{}" is not assigned to a quota.').format(
|
||||
str(validated_data.get('item'))
|
||||
gettext_lazy('There is not enough quota available on quota "{}" to perform '
|
||||
'the operation.').format(
|
||||
quota.name
|
||||
)
|
||||
)
|
||||
for quota in new_quotas:
|
||||
avail = quota.availability()
|
||||
if avail[0] != Quota.AVAILABILITY_OK or (avail[1] is not None and avail[1] < 1):
|
||||
raise ValidationError(
|
||||
gettext_lazy('There is not enough quota available on quota "{}" to perform '
|
||||
'the operation.').format(
|
||||
quota.name
|
||||
)
|
||||
)
|
||||
attendee_name = validated_data.pop('attendee_name', '')
|
||||
if attendee_name and not validated_data.get('attendee_name_parts'):
|
||||
validated_data['attendee_name_parts'] = {
|
||||
'_legacy': attendee_name
|
||||
}
|
||||
|
||||
seated = validated_data.get('item').seat_category_mappings.filter(subevent=validated_data.get('subevent')).exists()
|
||||
if validated_data.get('seat'):
|
||||
if not seated:
|
||||
raise ValidationError('The specified product does not allow to choose a seat.')
|
||||
try:
|
||||
seat = self.context['event'].seats.get(seat_guid=validated_data['seat'], subevent=validated_data.get('subevent'))
|
||||
except Seat.DoesNotExist:
|
||||
raise ValidationError('The specified seat does not exist.')
|
||||
except Seat.MultipleObjectsReturned:
|
||||
raise ValidationError('The specified seat ID is not unique.')
|
||||
else:
|
||||
validated_data['seat'] = seat
|
||||
if not seat.is_available(
|
||||
sales_channel=validated_data.get('sales_channel', 'web'),
|
||||
distance_ignore_cart_id=validated_data['cart_id'],
|
||||
):
|
||||
raise ValidationError(gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name))
|
||||
elif seated:
|
||||
raise ValidationError('The specified product requires to choose a seat.')
|
||||
for quota in new_quotas:
|
||||
oldsize = self.context['quota_cache'][quota.pk][1]
|
||||
newsize = oldsize - 1 if oldsize is not None else None
|
||||
self.context['quota_cache'][quota.pk] = (
|
||||
Quota.AVAILABILITY_OK if newsize is None or newsize > 0 else Quota.AVAILABILITY_GONE,
|
||||
newsize
|
||||
)
|
||||
|
||||
validated_data.pop('sales_channel')
|
||||
cp = CartPosition.objects.create(event=self.context['event'], **validated_data)
|
||||
attendee_name = validated_data.pop('attendee_name', '')
|
||||
if attendee_name and not validated_data.get('attendee_name_parts'):
|
||||
validated_data['attendee_name_parts'] = {
|
||||
'_legacy': attendee_name
|
||||
}
|
||||
|
||||
seated = validated_data.get('item').seat_category_mappings.filter(subevent=validated_data.get('subevent')).exists()
|
||||
if validated_data.get('seat'):
|
||||
if not seated:
|
||||
raise ValidationError('The specified product does not allow to choose a seat.')
|
||||
try:
|
||||
seat = self.context['event'].seats.get(seat_guid=validated_data['seat'], subevent=validated_data.get('subevent'))
|
||||
except Seat.DoesNotExist:
|
||||
raise ValidationError('The specified seat does not exist.')
|
||||
except Seat.MultipleObjectsReturned:
|
||||
raise ValidationError('The specified seat ID is not unique.')
|
||||
else:
|
||||
validated_data['seat'] = seat
|
||||
if not seat.is_available(
|
||||
sales_channel=validated_data.get('sales_channel', 'web'),
|
||||
distance_ignore_cart_id=validated_data['cart_id'],
|
||||
):
|
||||
raise ValidationError(gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name))
|
||||
elif seated:
|
||||
raise ValidationError('The specified product requires to choose a seat.')
|
||||
|
||||
validated_data.pop('sales_channel')
|
||||
cp = CartPosition.objects.create(event=self.context['event'], **validated_data)
|
||||
|
||||
for answ_data in answers_data:
|
||||
options = answ_data.pop('options')
|
||||
|
||||
@@ -60,7 +60,7 @@ class CheckinListSerializer(I18nAwareModelSerializer):
|
||||
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
||||
full_data.update(data)
|
||||
|
||||
for item in full_data.get('limit_products'):
|
||||
for item in full_data.get('limit_products', []):
|
||||
if event != item.event:
|
||||
raise ValidationError(_('One or more items do not belong to this event.'))
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext as _
|
||||
from django_countries.serializers import CountryFieldMixin
|
||||
from pytz import common_timezones
|
||||
from rest_framework import serializers
|
||||
from rest_framework.fields import ChoiceField, Field
|
||||
from rest_framework.relations import SlugRelatedField
|
||||
|
||||
@@ -53,7 +54,7 @@ from pretix.base.models.items import SubEventItem, SubEventItemVariation
|
||||
from pretix.base.services.seating import (
|
||||
SeatProtected, generate_seats, validate_plan_change,
|
||||
)
|
||||
from pretix.base.settings import validate_event_settings
|
||||
from pretix.base.settings import LazyI18nStringList, validate_event_settings
|
||||
from pretix.base.signals import api_event_settings_fields
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -93,9 +94,12 @@ class MetaPropertyField(Field):
|
||||
class SeatCategoryMappingField(Field):
|
||||
|
||||
def to_representation(self, value):
|
||||
qs = value.seat_category_mappings.all()
|
||||
if isinstance(value, Event):
|
||||
qs = qs.filter(subevent=None)
|
||||
if hasattr(value, '_seat_category_mappings'):
|
||||
qs = value._seat_category_mappings
|
||||
else:
|
||||
qs = value.seat_category_mappings.all()
|
||||
if isinstance(value, Event):
|
||||
qs = qs.filter(subevent=None)
|
||||
return {
|
||||
v.layout_category: v.product_id for v in qs
|
||||
}
|
||||
@@ -156,6 +160,7 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
seat_category_mapping = SeatCategoryMappingField(source='*', required=False)
|
||||
timezone = TimeZoneField(required=False, choices=[(a, a) for a in common_timezones])
|
||||
valid_keys = ValidKeysField(source='*', read_only=True)
|
||||
best_availability_state = serializers.IntegerField(allow_null=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Event
|
||||
@@ -163,12 +168,14 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
'date_to', 'date_admission', 'is_public', 'presale_start',
|
||||
'presale_end', 'location', 'geo_lat', 'geo_lon', 'has_subevents', 'meta_data', 'seating_plan',
|
||||
'plugins', 'seat_category_mapping', 'timezone', 'item_meta_properties', 'valid_keys',
|
||||
'sales_channels')
|
||||
'sales_channels', 'best_availability_state')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if not hasattr(self.context['request'], 'event'):
|
||||
self.fields.pop('valid_keys')
|
||||
if not self.context.get('request') or 'with_availability_for' not in self.context['request'].GET:
|
||||
self.fields.pop('best_availability_state')
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
@@ -441,13 +448,19 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
||||
seat_category_mapping = SeatCategoryMappingField(source='*', required=False)
|
||||
event = SlugRelatedField(slug_field='slug', read_only=True)
|
||||
meta_data = MetaDataField(source='*')
|
||||
best_availability_state = serializers.IntegerField(allow_null=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = SubEvent
|
||||
fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission',
|
||||
'presale_start', 'presale_end', 'location', 'geo_lat', 'geo_lon', 'event', 'is_public',
|
||||
'frontpage_text', 'seating_plan', 'item_price_overrides', 'variation_price_overrides',
|
||||
'meta_data', 'seat_category_mapping', 'last_modified')
|
||||
'meta_data', 'seat_category_mapping', 'last_modified', 'best_availability_state')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if not self.context.get('request') or 'with_availability_for' not in self.context['request'].GET:
|
||||
self.fields.pop('best_availability_state')
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
@@ -624,7 +637,7 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
||||
class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = TaxRule
|
||||
fields = ('id', 'name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country')
|
||||
fields = ('id', 'name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country', 'internal_name', 'keep_gross_if_rate_changes')
|
||||
|
||||
|
||||
class EventSettingsSerializer(SettingsSerializer):
|
||||
@@ -691,6 +704,7 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'payment_term_accept_late',
|
||||
'payment_explanation',
|
||||
'payment_pending_hidden',
|
||||
'mail_days_order_expire_warning',
|
||||
'ticket_download',
|
||||
'ticket_download_date',
|
||||
'ticket_download_addons',
|
||||
@@ -699,7 +713,6 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'ticket_download_require_validated_email',
|
||||
'ticket_secret_length',
|
||||
'mail_prefix',
|
||||
'mail_from',
|
||||
'mail_from_name',
|
||||
'mail_attach_ical',
|
||||
'mail_attach_tickets',
|
||||
@@ -720,6 +733,7 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'invoice_numbers_prefix_cancellations',
|
||||
'invoice_numbers_counter_length',
|
||||
'invoice_attendee_name',
|
||||
'invoice_event_location',
|
||||
'invoice_include_expire_date',
|
||||
'invoice_address_explanation_text',
|
||||
'invoice_email_attachment',
|
||||
@@ -749,6 +763,7 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'cancel_allow_user_paid_refund_as_giftcard',
|
||||
'cancel_allow_user_paid_require_approval',
|
||||
'change_allow_user_variation',
|
||||
'change_allow_user_addons',
|
||||
'change_allow_user_until',
|
||||
'change_allow_user_price',
|
||||
'primary_color',
|
||||
@@ -776,6 +791,10 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
data = super().validate(data)
|
||||
settings_dict = self.instance.freeze()
|
||||
settings_dict.update(data)
|
||||
|
||||
if data.get('confirm_texts') is not None:
|
||||
data['confirm_texts'] = LazyI18nStringList(data['confirm_texts'])
|
||||
|
||||
validate_event_settings(self.event, settings_dict)
|
||||
return data
|
||||
|
||||
|
||||
@@ -31,9 +31,10 @@
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import os.path
|
||||
from decimal import Decimal
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.db.models import QuerySet
|
||||
@@ -57,8 +58,10 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = ItemVariation
|
||||
fields = ('id', 'value', 'active', 'description',
|
||||
'position', 'default_price', 'price', 'original_price',
|
||||
'require_membership', 'require_membership_types',)
|
||||
'position', 'default_price', 'price', 'original_price', 'require_approval',
|
||||
'require_membership', 'require_membership_types',
|
||||
'require_membership_hidden', 'available_from', 'available_until',
|
||||
'sales_channels', 'hide_without_voucher',)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -72,8 +75,10 @@ class ItemVariationSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = ItemVariation
|
||||
fields = ('id', 'value', 'active', 'description',
|
||||
'position', 'default_price', 'price', 'original_price',
|
||||
'require_membership', 'require_membership_types',)
|
||||
'position', 'default_price', 'price', 'original_price', 'require_approval',
|
||||
'require_membership', 'require_membership_types',
|
||||
'require_membership_hidden', 'available_from', 'available_until',
|
||||
'sales_channels', 'hide_without_voucher',)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -161,7 +166,7 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
meta_data = MetaDataField(required=False, source='*')
|
||||
picture = UploadedFileField(required=False, allow_null=True, allowed_types=(
|
||||
'image/png', 'image/jpeg', 'image/gif'
|
||||
), max_size=10 * 1024 * 1024)
|
||||
), max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE)
|
||||
|
||||
class Meta:
|
||||
model = Item
|
||||
@@ -172,7 +177,7 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations', 'variations',
|
||||
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
|
||||
'show_quota_left', 'hidden_if_available', 'allow_waitinglist', 'issue_giftcard', 'meta_data',
|
||||
'require_membership', 'require_membership_types', 'grant_membership_type',
|
||||
'require_membership', 'require_membership_types', 'require_membership_hidden', 'grant_membership_type',
|
||||
'grant_membership_duration_like_event', 'grant_membership_duration_days',
|
||||
'grant_membership_duration_months')
|
||||
read_only_fields = ('has_variations',)
|
||||
@@ -245,10 +250,16 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
addons_data = validated_data.pop('addons') if 'addons' in validated_data else {}
|
||||
bundles_data = validated_data.pop('bundles') if 'bundles' in validated_data else {}
|
||||
meta_data = validated_data.pop('meta_data', None)
|
||||
picture = validated_data.pop('picture', None)
|
||||
require_membership_types = validated_data.pop('require_membership_types', [])
|
||||
item = Item.objects.create(**validated_data)
|
||||
if picture:
|
||||
item.picture.save(os.path.basename(picture.name), picture)
|
||||
if require_membership_types:
|
||||
item.require_membership_types.add(*require_membership_types)
|
||||
|
||||
for variation_data in variations_data:
|
||||
require_membership_types = variation_data.pop('require_membership_types')
|
||||
require_membership_types = variation_data.pop('require_membership_types', [])
|
||||
v = ItemVariation.objects.create(item=item, **variation_data)
|
||||
if require_membership_types:
|
||||
v.require_membership_types.add(*require_membership_types)
|
||||
@@ -269,7 +280,10 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
meta_data = validated_data.pop('meta_data', None)
|
||||
picture = validated_data.pop('picture', None)
|
||||
item = super().update(instance, validated_data)
|
||||
if picture:
|
||||
item.picture.save(os.path.basename(picture.name), picture)
|
||||
|
||||
# Meta data
|
||||
if meta_data is not None:
|
||||
@@ -408,10 +422,19 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
||||
|
||||
|
||||
class QuotaSerializer(I18nAwareModelSerializer):
|
||||
available = serializers.BooleanField(read_only=True)
|
||||
available_number = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Quota
|
||||
fields = ('id', 'name', 'size', 'items', 'variations', 'subevent', 'closed', 'close_when_sold_out', 'release_after_exit')
|
||||
fields = ('id', 'name', 'size', 'items', 'variations', 'subevent', 'closed', 'close_when_sold_out',
|
||||
'release_after_exit', 'available', 'available_number')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if 'request' not in self.context or self.context['request'].GET.get('with_availability') != 'true':
|
||||
del self.fields['available']
|
||||
del self.fields['available_number']
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
|
||||
@@ -26,6 +26,7 @@ from collections import Counter, defaultdict
|
||||
from decimal import Decimal
|
||||
|
||||
import pycountry
|
||||
from django.conf import settings
|
||||
from django.core.files import File
|
||||
from django.db.models import F, Q
|
||||
from django.utils.timezone import now
|
||||
@@ -191,7 +192,7 @@ class AnswerSerializer(I18nAwareModelSerializer):
|
||||
)
|
||||
if cf.type not in allowed_types:
|
||||
raise ValidationError('The submitted file "{fid}" has a file type that is not allowed in this field.'.format(fid=data))
|
||||
if cf.file.size > 10 * 1024 * 1024:
|
||||
if cf.file.size > settings.FILE_UPLOAD_MAX_SIZE_OTHER:
|
||||
raise ValidationError('The submitted file "{fid}" is too large to be used in this field.'.format(fid=data))
|
||||
|
||||
data['options'] = []
|
||||
@@ -199,7 +200,9 @@ class AnswerSerializer(I18nAwareModelSerializer):
|
||||
return data
|
||||
|
||||
def validate(self, data):
|
||||
if data.get('question').type == Question.TYPE_FILE:
|
||||
if not data.get('question'):
|
||||
raise ValidationError('Question not specified.')
|
||||
elif data.get('question').type == Question.TYPE_FILE:
|
||||
return self._handle_file_upload(data)
|
||||
elif data.get('question').type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
|
||||
if not data.get('options'):
|
||||
@@ -250,7 +253,30 @@ class AnswerSerializer(I18nAwareModelSerializer):
|
||||
class CheckinSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = Checkin
|
||||
fields = ('id', 'datetime', 'list', 'auto_checked_in', 'type')
|
||||
fields = ('id', 'datetime', 'list', 'auto_checked_in', 'gate', 'device', 'type')
|
||||
|
||||
|
||||
class FailedCheckinSerializer(I18nAwareModelSerializer):
|
||||
error_reason = serializers.ChoiceField(choices=Checkin.REASONS, required=True, allow_null=False)
|
||||
raw_barcode = serializers.CharField(required=True, allow_null=False)
|
||||
position = serializers.PrimaryKeyRelatedField(queryset=OrderPosition.all.none(), required=False, allow_null=True)
|
||||
raw_item = serializers.PrimaryKeyRelatedField(queryset=Item.objects.none(), required=False, allow_null=True)
|
||||
raw_variation = serializers.PrimaryKeyRelatedField(queryset=ItemVariation.objects.none(), required=False, allow_null=True)
|
||||
raw_subevent = serializers.PrimaryKeyRelatedField(queryset=SubEvent.objects.none(), required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = Checkin
|
||||
fields = ('error_reason', 'error_explanation', 'raw_barcode', 'raw_item', 'raw_variation',
|
||||
'raw_subevent', 'datetime', 'type', 'position')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
event = self.context['event']
|
||||
self.fields['raw_item'].queryset = event.items.all()
|
||||
self.fields['raw_variation'].queryset = ItemVariation.objects.filter(item__event=event)
|
||||
self.fields['position'].queryset = OrderPosition.all.filter(order__event=event)
|
||||
if event.has_subevents:
|
||||
self.fields['raw_subevent'].queryset = event.subevents.all()
|
||||
|
||||
|
||||
class OrderDownloadsField(serializers.Field):
|
||||
@@ -631,7 +657,7 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
model = Order
|
||||
fields = (
|
||||
'code', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
|
||||
'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads',
|
||||
'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads',
|
||||
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
|
||||
'url', 'customer'
|
||||
)
|
||||
@@ -662,7 +688,7 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
def update(self, instance, validated_data):
|
||||
# Even though all fields that shouldn't be edited are marked as read_only in the serializer
|
||||
# (hopefully), we'll be extra careful here and be explicit about the model fields we update.
|
||||
update_fields = ['comment', 'checkin_attention', 'email', 'locale', 'phone']
|
||||
update_fields = ['comment', 'custom_followup_at', 'checkin_attention', 'email', 'locale', 'phone']
|
||||
|
||||
if 'invoice_address' in validated_data:
|
||||
iadata = validated_data.pop('invoice_address')
|
||||
@@ -902,12 +928,14 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
min_length=5
|
||||
)
|
||||
comment = serializers.CharField(required=False, allow_blank=True)
|
||||
custom_followup_at = serializers.DateField(required=False, allow_null=True)
|
||||
payment_provider = serializers.CharField(required=False, allow_null=True)
|
||||
payment_info = CompatibleJSONField(required=False)
|
||||
consume_carts = serializers.ListField(child=serializers.CharField(), required=False)
|
||||
force = serializers.BooleanField(default=False, required=False)
|
||||
payment_date = serializers.DateTimeField(required=False, allow_null=True)
|
||||
send_email = serializers.BooleanField(default=False, required=False)
|
||||
send_email = serializers.BooleanField(default=False, required=False, allow_null=True)
|
||||
require_approval = serializers.BooleanField(default=False, required=False)
|
||||
simulate = serializers.BooleanField(default=False, required=False)
|
||||
customer = serializers.SlugRelatedField(slug_field='identifier', queryset=Customer.objects.none(), required=False)
|
||||
|
||||
@@ -920,7 +948,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
model = Order
|
||||
fields = ('code', 'status', 'testmode', 'email', 'phone', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
|
||||
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts',
|
||||
'force', 'send_email', 'simulate', 'customer')
|
||||
'force', 'send_email', 'simulate', 'customer', 'custom_followup_at', 'require_approval')
|
||||
|
||||
def validate_payment_provider(self, pp):
|
||||
if pp is None:
|
||||
@@ -1014,6 +1042,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
force = validated_data.pop('force', False)
|
||||
simulate = validated_data.pop('simulate', False)
|
||||
self._send_mail = validated_data.pop('send_email', False)
|
||||
if self._send_mail is None:
|
||||
self._send_mail = validated_data.get('sales_channel') in self.context['event'].settings.mail_sales_channel_placed_paid
|
||||
|
||||
if 'invoice_address' in validated_data:
|
||||
iadata = validated_data.pop('invoice_address')
|
||||
@@ -1192,6 +1222,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
|
||||
order.meta_info = "{}"
|
||||
order.total = Decimal('0.00')
|
||||
if validated_data.get('require_approval') is not None:
|
||||
order.require_approval = validated_data['require_approval']
|
||||
if simulate:
|
||||
order = WrappedModel(order)
|
||||
order.last_modified = now()
|
||||
@@ -1269,7 +1301,13 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
if pos.voucher:
|
||||
Voucher.objects.filter(pk=pos.voucher.pk).update(redeemed=F('redeemed') + 1)
|
||||
pos.save()
|
||||
seen_answers = set()
|
||||
for answ_data in answers_data:
|
||||
# Workaround for a pretixPOS bug :-(
|
||||
if answ_data.get('question') in seen_answers:
|
||||
continue
|
||||
seen_answers.add(answ_data.get('question'))
|
||||
|
||||
options = answ_data.pop('options', [])
|
||||
|
||||
if isinstance(answ_data['answer'], File):
|
||||
@@ -1371,6 +1409,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
state=OrderPayment.PAYMENT_STATE_CREATED
|
||||
)
|
||||
|
||||
order.create_transactions(is_new=True, fees=fees, positions=pos_map.values())
|
||||
return order
|
||||
|
||||
|
||||
@@ -1392,8 +1431,9 @@ class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = InvoiceLine
|
||||
fields = ('position', 'description', 'item', 'variation', 'attendee_name', 'event_date_from',
|
||||
'event_date_to', 'gross_value', 'tax_value', 'tax_rate', 'tax_name')
|
||||
fields = ('position', 'description', 'item', 'variation', 'subevent', 'attendee_name', 'event_date_from',
|
||||
'event_date_to', 'gross_value', 'tax_value', 'tax_rate', 'tax_name', 'fee_type',
|
||||
'fee_internal_type', 'event_location')
|
||||
|
||||
|
||||
class InvoiceSerializer(I18nAwareModelSerializer):
|
||||
|
||||
@@ -275,6 +275,7 @@ class OrganizerSettingsSerializer(SettingsSerializer):
|
||||
default_fields = [
|
||||
'customer_accounts',
|
||||
'customer_accounts_link_by_email',
|
||||
'invoice_regenerate_allowed',
|
||||
'contact_mail',
|
||||
'imprint_url',
|
||||
'organizer_info_text',
|
||||
@@ -294,7 +295,15 @@ class OrganizerSettingsSerializer(SettingsSerializer):
|
||||
'theme_color_background',
|
||||
'theme_round_borders',
|
||||
'primary_font',
|
||||
'organizer_logo_image'
|
||||
'organizer_logo_image_inherit',
|
||||
'organizer_logo_image',
|
||||
'privacy_url',
|
||||
'cookie_consent',
|
||||
'cookie_consent_dialog_title',
|
||||
'cookie_consent_dialog_text',
|
||||
'cookie_consent_dialog_text_secondary',
|
||||
'cookie_consent_dialog_button_yes',
|
||||
'cookie_consent_dialog_button_no',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@@ -54,8 +54,8 @@ class SettingsSerializer(serializers.Serializer):
|
||||
f = DEFAULTS[fname]['serializer_class'](
|
||||
**kwargs
|
||||
)
|
||||
f._label = form_kwargs.get('label', fname)
|
||||
f._help_text = form_kwargs.get('help_text')
|
||||
f._label = str(form_kwargs.get('label', fname))
|
||||
f._help_text = str(form_kwargs.get('help_text'))
|
||||
f.parent = self
|
||||
self.fields[fname] = f
|
||||
|
||||
|
||||
@@ -21,14 +21,18 @@
|
||||
#
|
||||
from django.db import transaction
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.filters import OrderingFilter
|
||||
from rest_framework.mixins import CreateModelMixin, DestroyModelMixin
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
|
||||
from pretix.api.serializers.cart import (
|
||||
CartPositionCreateSerializer, CartPositionSerializer,
|
||||
)
|
||||
from pretix.base.models import CartPosition
|
||||
from pretix.base.services.locking import NoLockManager
|
||||
|
||||
|
||||
class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
@@ -50,18 +54,61 @@ class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnly
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['event'] = self.request.event
|
||||
ctx['quota_cache'] = {}
|
||||
return ctx
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = CartPositionCreateSerializer(data=request.data, context=self.get_serializer_context())
|
||||
serializer.is_valid(raise_exception=True)
|
||||
with transaction.atomic():
|
||||
with transaction.atomic(), self.request.event.lock():
|
||||
self.perform_create(serializer)
|
||||
cp = serializer.instance
|
||||
serializer = CartPositionSerializer(cp, context=serializer.context)
|
||||
|
||||
cp = serializer.instance
|
||||
serializer = CartPositionSerializer(cp, context=serializer.context)
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
|
||||
@action(detail=False, methods=['POST'])
|
||||
def bulk_create(self, request, *args, **kwargs):
|
||||
if not isinstance(request.data, list): # noqa
|
||||
return Response({"error": "Please supply a list"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
ctx = self.get_serializer_context()
|
||||
with transaction.atomic():
|
||||
serializers = [
|
||||
CartPositionCreateSerializer(data=d, context=ctx)
|
||||
for d in request.data
|
||||
]
|
||||
|
||||
lockfn = self.request.event.lock
|
||||
if not any(s.is_valid(raise_exception=False) for s in serializers):
|
||||
lockfn = NoLockManager
|
||||
|
||||
results = []
|
||||
with lockfn():
|
||||
for s in serializers:
|
||||
if s.is_valid(raise_exception=False):
|
||||
try:
|
||||
cp = s.save()
|
||||
except ValidationError as e:
|
||||
results.append({
|
||||
'success': False,
|
||||
'data': None,
|
||||
'errors': {api_settings.NON_FIELD_ERRORS_KEY: e.detail},
|
||||
})
|
||||
else:
|
||||
results.append({
|
||||
'success': True,
|
||||
'data': CartPositionSerializer(cp, context=ctx).data,
|
||||
'errors': None,
|
||||
})
|
||||
else:
|
||||
results.append({
|
||||
'success': False,
|
||||
'data': None,
|
||||
'errors': s.errors,
|
||||
})
|
||||
|
||||
return Response({'results': results}, status=status.HTTP_200_OK)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save()
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import django_filters
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.db.models import (
|
||||
Count, Exists, F, Max, OrderBy, OuterRef, Prefetch, Q, Subquery,
|
||||
)
|
||||
@@ -31,19 +33,24 @@ from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from packaging.version import parse
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import DateTimeField
|
||||
from rest_framework.permissions import SAFE_METHODS
|
||||
from rest_framework.response import Response
|
||||
|
||||
from pretix.api.serializers.checkin import CheckinListSerializer
|
||||
from pretix.api.serializers.item import QuestionSerializer
|
||||
from pretix.api.serializers.order import CheckinListOrderPositionSerializer
|
||||
from pretix.api.serializers.order import (
|
||||
CheckinListOrderPositionSerializer, FailedCheckinSerializer,
|
||||
)
|
||||
from pretix.api.views import RichOrderingFilter
|
||||
from pretix.api.views.order import OrderPositionFilter
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedFile, Checkin, CheckinList, Event, Order, OrderPosition, Question,
|
||||
CachedFile, Checkin, CheckinList, Device, Event, Order, OrderPosition,
|
||||
Question,
|
||||
)
|
||||
from pretix.base.services.checkin import (
|
||||
CheckInError, RequiredQuestionsError, SQLLogic, perform_checkin,
|
||||
@@ -79,8 +86,14 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
queryset = CheckinList.objects.none()
|
||||
filter_backends = (DjangoFilterBackend,)
|
||||
filterset_class = CheckinListFilter
|
||||
permission = ('can_view_orders', 'can_checkin_orders',)
|
||||
write_permission = 'can_change_event_settings'
|
||||
|
||||
def _get_permission_name(self, request):
|
||||
if request.path.endswith('/failed_checkins/'):
|
||||
return 'can_checkin_orders', 'can_change_orders'
|
||||
elif request.method in SAFE_METHODS:
|
||||
return 'can_view_orders', 'can_checkin_orders',
|
||||
else:
|
||||
return 'can_change_event_settings'
|
||||
|
||||
def get_queryset(self):
|
||||
qs = self.request.event.checkin_lists.prefetch_related(
|
||||
@@ -125,6 +138,49 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
super().perform_destroy(instance)
|
||||
|
||||
@action(detail=True, methods=['POST'], url_name='failed_checkins')
|
||||
@transaction.atomic()
|
||||
def failed_checkins(self, *args, **kwargs):
|
||||
serializer = FailedCheckinSerializer(
|
||||
data=self.request.data,
|
||||
context={'event': self.request.event}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
kwargs = {}
|
||||
|
||||
if not serializer.validated_data.get('position'):
|
||||
kwargs['position'] = OrderPosition.all.filter(
|
||||
secret=serializer.validated_data['raw_barcode']
|
||||
).first()
|
||||
|
||||
c = serializer.save(
|
||||
list=self.get_object(),
|
||||
successful=False,
|
||||
forced=True,
|
||||
device=self.request.auth if isinstance(self.request.auth, Device) else None,
|
||||
gate=self.request.auth.gate if isinstance(self.request.auth, Device) else None,
|
||||
**kwargs,
|
||||
)
|
||||
if c.position:
|
||||
c.position.order.log_action('pretix.event.checkin.denied', data={
|
||||
'position': c.position.id,
|
||||
'positionid': c.position.positionid,
|
||||
'errorcode': c.error_reason,
|
||||
'reason_explanation': c.error_explanation,
|
||||
'datetime': c.datetime,
|
||||
'type': c.type,
|
||||
'list': c.list.pk
|
||||
}, user=self.request.user, auth=self.request.auth)
|
||||
else:
|
||||
self.request.event.log_action('pretix.event.checkin.unknown', data={
|
||||
'datetime': c.datetime,
|
||||
'type': c.type,
|
||||
'list': c.list.pk,
|
||||
'barcode': c.raw_barcode
|
||||
}, user=self.request.user, auth=self.request.auth)
|
||||
|
||||
return Response(serializer.data, status=201)
|
||||
|
||||
@action(detail=True, methods=['GET'])
|
||||
def status(self, *args, **kwargs):
|
||||
with language(self.request.event.settings.locale):
|
||||
@@ -294,7 +350,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
lookup='checkins',
|
||||
queryset=Checkin.objects.filter(list_id=self.checkinlist.pk)
|
||||
),
|
||||
'checkins', 'answers', 'answers__options', 'answers__question',
|
||||
'answers', 'answers__options', 'answers__question',
|
||||
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')),
|
||||
Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related(
|
||||
Prefetch(
|
||||
@@ -304,7 +360,8 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
Prefetch(
|
||||
'positions',
|
||||
OrderPosition.objects.prefetch_related(
|
||||
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
||||
Prefetch('checkins', queryset=Checkin.objects.all()),
|
||||
'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
||||
)
|
||||
)
|
||||
))
|
||||
@@ -356,30 +413,113 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
else:
|
||||
dt = now()
|
||||
|
||||
common_checkin_args = dict(
|
||||
raw_barcode=self.kwargs['pk'],
|
||||
type=type,
|
||||
list=self.checkinlist,
|
||||
datetime=dt,
|
||||
device=self.request.auth if isinstance(self.request.auth, Device) else None,
|
||||
gate=self.request.auth.gate if isinstance(self.request.auth, Device) else None,
|
||||
nonce=nonce,
|
||||
forced=force,
|
||||
)
|
||||
raw_barcode_for_checkin = None
|
||||
|
||||
try:
|
||||
queryset = self.get_queryset(ignore_status=True, ignore_products=True)
|
||||
if self.kwargs['pk'].isnumeric():
|
||||
op = queryset.get(Q(pk=self.kwargs['pk']) | Q(secret=self.kwargs['pk']))
|
||||
else:
|
||||
op = queryset.get(secret=self.kwargs['pk'])
|
||||
# In application/x-www-form-urlencoded, you can encodes space ' ' with '+' instead of '%20'.
|
||||
# `id`, however, is part of a path where this technically is not allowed. Old versions of our
|
||||
# scan apps still do it, so we try work around it!
|
||||
try:
|
||||
op = queryset.get(secret=self.kwargs['pk'])
|
||||
except OrderPosition.DoesNotExist:
|
||||
op = queryset.get(secret=self.kwargs['pk'].replace('+', ' '))
|
||||
except OrderPosition.DoesNotExist:
|
||||
revoked_matches = list(self.request.event.revoked_secrets.filter(secret=self.kwargs['pk']))
|
||||
if len(revoked_matches) == 0 or not force:
|
||||
if len(revoked_matches) == 0:
|
||||
self.request.event.log_action('pretix.event.checkin.unknown', data={
|
||||
'datetime': dt,
|
||||
'type': type,
|
||||
'list': self.checkinlist.pk,
|
||||
'barcode': self.kwargs['pk']
|
||||
}, user=self.request.user, auth=self.request.auth)
|
||||
raise Http404()
|
||||
|
||||
op = revoked_matches[0].position
|
||||
op.order.log_action('pretix.event.checkin.revoked', data={
|
||||
'datetime': dt,
|
||||
'type': type,
|
||||
'list': self.checkinlist.pk,
|
||||
'barcode': self.kwargs['pk']
|
||||
}, user=self.request.user, auth=self.request.auth)
|
||||
for k, s in self.request.event.ticket_secret_generators.items():
|
||||
try:
|
||||
parsed = s.parse_secret(self.kwargs['pk'])
|
||||
common_checkin_args.update({
|
||||
'raw_item': parsed.item,
|
||||
'raw_variation': parsed.variation,
|
||||
'raw_subevent': parsed.subevent,
|
||||
})
|
||||
except:
|
||||
pass
|
||||
|
||||
Checkin.objects.create(
|
||||
position=None,
|
||||
successful=False,
|
||||
error_reason=Checkin.REASON_INVALID,
|
||||
**common_checkin_args,
|
||||
)
|
||||
|
||||
if force and isinstance(self.request.auth, Device):
|
||||
# There was a bug in libpretixsync: If you scanned a ticket in offline mode that was
|
||||
# valid at the time but no longer exists at time of upload, the device would retry to
|
||||
# upload the same scan over and over again. Since we can't update all devices quickly,
|
||||
# here's a dirty workaround to make it stop.
|
||||
try:
|
||||
brand = self.request.auth.software_brand
|
||||
ver = parse(self.request.auth.software_version)
|
||||
legacy_mode = (
|
||||
(brand == 'pretixSCANPROXY' and ver < parse('0.0.3')) or
|
||||
(brand == 'pretixSCAN Android' and ver < parse('1.11.2')) or
|
||||
(brand == 'pretixSCAN' and ver < parse('1.11.2'))
|
||||
)
|
||||
if legacy_mode:
|
||||
return Response({
|
||||
'status': 'error',
|
||||
'reason': Checkin.REASON_ALREADY_REDEEMED,
|
||||
'reason_explanation': None,
|
||||
'require_attention': False,
|
||||
'__warning': 'Compatibility hack active due to detected old pretixSCAN version',
|
||||
}, status=400)
|
||||
except: # we don't care e.g. about invalid version numbers
|
||||
pass
|
||||
|
||||
return Response({
|
||||
'detail': 'Not found.', # for backwards compatibility
|
||||
'status': 'error',
|
||||
'reason': Checkin.REASON_INVALID,
|
||||
'reason_explanation': None,
|
||||
'require_attention': False,
|
||||
}, status=404)
|
||||
elif revoked_matches and force:
|
||||
op = revoked_matches[0].position
|
||||
raw_barcode_for_checkin = self.kwargs['pk']
|
||||
else:
|
||||
op = revoked_matches[0].position
|
||||
op.order.log_action('pretix.event.checkin.revoked', data={
|
||||
'datetime': dt,
|
||||
'type': type,
|
||||
'list': self.checkinlist.pk,
|
||||
'barcode': self.kwargs['pk']
|
||||
}, user=self.request.user, auth=self.request.auth)
|
||||
Checkin.objects.create(
|
||||
position=op,
|
||||
successful=False,
|
||||
error_reason=Checkin.REASON_REVOKED,
|
||||
**common_checkin_args
|
||||
)
|
||||
return Response({
|
||||
'status': 'error',
|
||||
'reason': Checkin.REASON_REVOKED,
|
||||
'reason_explanation': None,
|
||||
'require_attention': False,
|
||||
'position': CheckinListOrderPositionSerializer(op, context=self.get_serializer_context()).data
|
||||
}, status=400)
|
||||
|
||||
given_answers = {}
|
||||
if 'answers' in self.request.data:
|
||||
@@ -409,6 +549,8 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
type=type,
|
||||
raw_barcode=raw_barcode_for_checkin,
|
||||
from_revoked_secret=True,
|
||||
)
|
||||
except RequiredQuestionsError as e:
|
||||
return Response({
|
||||
@@ -424,11 +566,19 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
'position': op.id,
|
||||
'positionid': op.positionid,
|
||||
'errorcode': e.code,
|
||||
'reason_explanation': e.reason,
|
||||
'force': force,
|
||||
'datetime': dt,
|
||||
'type': type,
|
||||
'list': self.checkinlist.pk
|
||||
}, user=self.request.user, auth=self.request.auth)
|
||||
Checkin.objects.create(
|
||||
position=op,
|
||||
successful=False,
|
||||
error_reason=e.code,
|
||||
error_explanation=e.reason,
|
||||
**common_checkin_args,
|
||||
)
|
||||
return Response({
|
||||
'status': 'error',
|
||||
'reason': e.code,
|
||||
@@ -460,7 +610,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
)
|
||||
if cf.type not in allowed_types:
|
||||
raise ValidationError('The submitted file "{fid}" has a file type that is not allowed in this field.'.format(fid=data))
|
||||
if cf.file.size > 10 * 1024 * 1024:
|
||||
if cf.file.size > settings.FILE_UPLOAD_MAX_SIZE_OTHER:
|
||||
raise ValidationError('The submitted file "{fid}" is too large to be used in this field.'.format(fid=data))
|
||||
|
||||
return cf.file
|
||||
|
||||
@@ -42,6 +42,7 @@ class InitializationRequestSerializer(serializers.Serializer):
|
||||
hardware_model = serializers.CharField(max_length=190)
|
||||
software_brand = serializers.CharField(max_length=190)
|
||||
software_version = serializers.CharField(max_length=190)
|
||||
info = serializers.JSONField(required=False, allow_null=True)
|
||||
|
||||
|
||||
class UpdateRequestSerializer(serializers.Serializer):
|
||||
@@ -49,6 +50,7 @@ class UpdateRequestSerializer(serializers.Serializer):
|
||||
hardware_model = serializers.CharField(max_length=190)
|
||||
software_brand = serializers.CharField(max_length=190)
|
||||
software_version = serializers.CharField(max_length=190)
|
||||
info = serializers.JSONField(required=False, allow_null=True)
|
||||
|
||||
|
||||
class GateSerializer(serializers.ModelSerializer):
|
||||
@@ -94,6 +96,7 @@ class InitializeView(APIView):
|
||||
device.hardware_model = serializer.validated_data.get('hardware_model')
|
||||
device.software_brand = serializer.validated_data.get('software_brand')
|
||||
device.software_version = serializer.validated_data.get('software_version')
|
||||
device.info = serializer.validated_data.get('info')
|
||||
device.api_token = generate_api_token()
|
||||
device.save()
|
||||
|
||||
@@ -114,6 +117,7 @@ class UpdateView(APIView):
|
||||
device.hardware_model = serializer.validated_data.get('hardware_model')
|
||||
device.software_brand = serializer.validated_data.get('software_brand')
|
||||
device.software_version = serializer.validated_data.get('software_version')
|
||||
device.info = serializer.validated_data.get('info')
|
||||
device.save()
|
||||
device.log_action('pretix.device.updated', data=serializer.validated_data, auth=device)
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
import django_filters
|
||||
from django.db import transaction
|
||||
from django.db.models import ProtectedError, Q
|
||||
from django.db.models import Prefetch, ProtectedError, Q
|
||||
from django.utils.timezone import now
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
@@ -49,20 +49,24 @@ from pretix.api.serializers.event import (
|
||||
)
|
||||
from pretix.api.views import ConditionalListView
|
||||
from pretix.base.models import (
|
||||
CartPosition, Device, Event, TaxRule, TeamAPIToken,
|
||||
CartPosition, Device, Event, SeatCategoryMapping, TaxRule, TeamAPIToken,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.services.quotas import QuotaAvailability
|
||||
from pretix.base.settings import SETTINGS_AFFECTING_CSS
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
from pretix.helpers.i18n import i18ncomp
|
||||
from pretix.presale.style import regenerate_css
|
||||
from pretix.presale.views.organizer import filter_qs_by_attr
|
||||
|
||||
with scopes_disabled():
|
||||
class EventFilter(FilterSet):
|
||||
|
||||
is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs')
|
||||
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
|
||||
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
|
||||
sales_channel = django_filters.rest_framework.CharFilter(method='sales_channel_qs')
|
||||
search = django_filters.rest_framework.CharFilter(method='search_qs')
|
||||
|
||||
class Meta:
|
||||
model = Event
|
||||
@@ -107,6 +111,13 @@ with scopes_disabled():
|
||||
def sales_channel_qs(self, queryset, name, value):
|
||||
return queryset.filter(sales_channels__contains=value)
|
||||
|
||||
def search_qs(self, queryset, name, value):
|
||||
return queryset.filter(
|
||||
Q(name__icontains=i18ncomp(value))
|
||||
| Q(slug__icontains=value)
|
||||
| Q(location__icontains=i18ncomp(value))
|
||||
)
|
||||
|
||||
|
||||
class EventViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = EventSerializer
|
||||
@@ -136,10 +147,43 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
|
||||
qs = filter_qs_by_attr(qs, self.request)
|
||||
|
||||
if 'with_availability_for' in self.request.GET:
|
||||
qs = Event.annotated(qs, channel=self.request.GET.get('with_availability_for'))
|
||||
|
||||
return qs.prefetch_related(
|
||||
'meta_values', 'meta_values__property', 'seat_category_mappings'
|
||||
'organizer',
|
||||
'meta_values',
|
||||
'meta_values__property',
|
||||
'item_meta_properties',
|
||||
Prefetch(
|
||||
'seat_category_mappings',
|
||||
to_attr='_seat_category_mappings',
|
||||
queryset=SeatCategoryMapping.objects.filter(subevent=None)
|
||||
),
|
||||
)
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
|
||||
page = self.paginate_queryset(queryset)
|
||||
|
||||
if 'with_availability_for' in self.request.GET:
|
||||
quotas_to_compute = []
|
||||
qcache = {}
|
||||
for se in page:
|
||||
se._quota_cache = qcache
|
||||
quotas_to_compute += se.active_quotas
|
||||
|
||||
if quotas_to_compute:
|
||||
qa = QuotaAvailability()
|
||||
qa.queue(*quotas_to_compute)
|
||||
qa.compute(allow_cache=True)
|
||||
qcache.update(qa.results)
|
||||
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
current_live_value = serializer.instance.live
|
||||
updated_live_value = serializer.validated_data.get('live', None)
|
||||
@@ -197,7 +241,6 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
except Event.DoesNotExist:
|
||||
raise ValidationError('Event to copy from was not found')
|
||||
|
||||
print(copy_from, self.request.GET)
|
||||
new_event = serializer.save(organizer=self.request.organizer)
|
||||
|
||||
if copy_from:
|
||||
@@ -336,8 +379,18 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
|
||||
qs = filter_qs_by_attr(qs, self.request)
|
||||
|
||||
if 'with_availability_for' in self.request.GET:
|
||||
qs = SubEvent.annotated(qs, channel=self.request.GET.get('with_availability_for'))
|
||||
|
||||
return qs.prefetch_related(
|
||||
'subeventitem_set', 'subeventitemvariation_set', 'seat_category_mappings', 'meta_values'
|
||||
'event',
|
||||
'subeventitem_set',
|
||||
'subeventitemvariation_set',
|
||||
'meta_values',
|
||||
Prefetch(
|
||||
'seat_category_mappings',
|
||||
to_attr='_seat_category_mappings',
|
||||
),
|
||||
)
|
||||
|
||||
def list(self, request, **kwargs):
|
||||
@@ -345,14 +398,24 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
resp = self.get_paginated_response(serializer.data)
|
||||
resp['X-Page-Generated'] = date
|
||||
return resp
|
||||
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data, headers={'X-Page-Generated': date})
|
||||
if 'with_availability_for' in self.request.GET:
|
||||
quotas_to_compute = []
|
||||
qcache = {}
|
||||
for se in page:
|
||||
se._quota_cache = qcache
|
||||
quotas_to_compute += se.active_quotas
|
||||
|
||||
if quotas_to_compute:
|
||||
qa = QuotaAvailability()
|
||||
qa.queue(*quotas_to_compute)
|
||||
qa.compute(allow_cache=True)
|
||||
qcache.update(qa.results)
|
||||
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
resp = self.get_paginated_response(serializer.data)
|
||||
resp['X-Page-Generated'] = date
|
||||
return resp
|
||||
|
||||
def perform_update(self, serializer):
|
||||
original_data = self.get_serializer(instance=serializer.instance).data
|
||||
|
||||
@@ -69,7 +69,7 @@ class ExportersMixin:
|
||||
cf = get_object_or_404(CachedFile, id=kwargs['cfid'])
|
||||
if cf.file:
|
||||
resp = ChunkBasedFileResponse(cf.file.file, content_type=cf.type)
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename)
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename).encode("ascii", "ignore")
|
||||
return resp
|
||||
elif not settings.HAS_CELERY:
|
||||
return Response(
|
||||
@@ -132,7 +132,7 @@ class EventExportersViewSet(ExportersMixin, viewsets.ViewSet):
|
||||
def exporters(self):
|
||||
exporters = []
|
||||
responses = register_data_exporters.send(self.request.event)
|
||||
for ex in sorted([response(self.request.event) for r, response in responses], key=lambda ex: str(ex.verbose_name)):
|
||||
for ex in sorted([response(self.request.event, self.request.organizer) for r, response in responses if response], key=lambda ex: str(ex.verbose_name)):
|
||||
ex._serializer = JobRunSerializer(exporter=ex)
|
||||
exporters.append(ex)
|
||||
return exporters
|
||||
@@ -147,18 +147,26 @@ class OrganizerExportersViewSet(ExportersMixin, viewsets.ViewSet):
|
||||
@cached_property
|
||||
def exporters(self):
|
||||
exporters = []
|
||||
events = (self.request.auth or self.request.user).get_events_with_permission('can_view_orders', request=self.request).filter(
|
||||
if isinstance(self.request.auth, (Device, TeamAPIToken)):
|
||||
perm_holder = self.request.auth
|
||||
else:
|
||||
perm_holder = self.request.user
|
||||
events = perm_holder.get_events_with_permission('can_view_orders', request=self.request).filter(
|
||||
organizer=self.request.organizer
|
||||
)
|
||||
responses = register_multievent_data_exporters.send(self.request.organizer)
|
||||
for ex in sorted([response(events) for r, response in responses if response], key=lambda ex: str(ex.verbose_name)):
|
||||
for ex in sorted([response(events, self.request.organizer) for r, response in responses if response], key=lambda ex: str(ex.verbose_name)):
|
||||
ex._serializer = JobRunSerializer(exporter=ex, events=events)
|
||||
exporters.append(ex)
|
||||
return exporters
|
||||
|
||||
def get_serializer_kwargs(self):
|
||||
if isinstance(self.request.auth, (Device, TeamAPIToken)):
|
||||
perm_holder = self.request.auth
|
||||
else:
|
||||
perm_holder = self.request.user
|
||||
return {
|
||||
'events': self.request.auth.get_events_with_permission('can_view_orders', request=self.request).filter(
|
||||
'events': perm_holder.get_events_with_permission('can_view_orders', request=self.request).filter(
|
||||
organizer=self.request.organizer
|
||||
)
|
||||
}
|
||||
|
||||
@@ -477,6 +477,23 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
def get_queryset(self):
|
||||
return self.request.event.quotas.all()
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
|
||||
page = self.paginate_queryset(queryset)
|
||||
|
||||
if self.request.GET.get('with_availability') == 'true':
|
||||
if page:
|
||||
qa = QuotaAvailability()
|
||||
qa.queue(*page)
|
||||
qa.compute(allow_cache=False)
|
||||
for q in page:
|
||||
q.available = qa.results[q][0] == Quota.AVAILABILITY_OK
|
||||
q.available_number = qa.results[q][1]
|
||||
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
@@ -496,6 +513,7 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['event'] = self.request.event
|
||||
ctx['request'] = self.request
|
||||
return ctx
|
||||
|
||||
def perform_update(self, serializer):
|
||||
|
||||
@@ -55,9 +55,9 @@ from pretix.api.serializers.order import (
|
||||
)
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedCombinedTicket, CachedTicket, Device, Event, Invoice, InvoiceAddress,
|
||||
Order, OrderFee, OrderPayment, OrderPosition, OrderRefund, Quota, SubEvent,
|
||||
TaxRule, TeamAPIToken, generate_secret,
|
||||
CachedCombinedTicket, CachedTicket, Checkin, Device, Event, Invoice,
|
||||
InvoiceAddress, Order, OrderFee, OrderPayment, OrderPosition, OrderRefund,
|
||||
Quota, SubEvent, TaxRule, TeamAPIToken, generate_secret,
|
||||
)
|
||||
from pretix.base.models.orders import QuestionAnswer, RevokedTicketSecret
|
||||
from pretix.base.payment import PaymentException
|
||||
@@ -92,6 +92,9 @@ with scopes_disabled():
|
||||
subevent_after = django_filters.IsoDateTimeFilter(method='subevent_after_qs')
|
||||
subevent_before = django_filters.IsoDateTimeFilter(method='subevent_before_qs')
|
||||
search = django_filters.CharFilter(method='search_qs')
|
||||
item = django_filters.CharFilter(field_name='all_positions', lookup_expr='item_id')
|
||||
variation = django_filters.CharFilter(field_name='all_positions', lookup_expr='variation_id')
|
||||
subevent = django_filters.CharFilter(field_name='all_positions', lookup_expr='subevent_id')
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
@@ -201,7 +204,8 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
Prefetch(
|
||||
'positions',
|
||||
opq.all().prefetch_related(
|
||||
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
||||
Prefetch('checkins', queryset=Checkin.objects.all()),
|
||||
'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
||||
'item__category', 'addon_to', 'seat',
|
||||
Prefetch('addons', opq.select_related('item', 'variation', 'seat'))
|
||||
)
|
||||
@@ -212,7 +216,10 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
Prefetch(
|
||||
'positions',
|
||||
opq.all().prefetch_related(
|
||||
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question', 'seat',
|
||||
Prefetch('checkins', queryset=Checkin.objects.all()),
|
||||
'item', 'variation',
|
||||
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options', 'question').order_by('question__position')),
|
||||
'seat',
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -639,7 +646,11 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
payment and order.total == Decimal('0.00') and order.status == Order.STATUS_PAID and
|
||||
not order.require_approval and payment.provider == "free"
|
||||
)
|
||||
if free_flow:
|
||||
if order.require_approval:
|
||||
email_template = request.event.settings.mail_text_order_placed_require_approval
|
||||
log_entry = 'pretix.event.order.email.order_placed_require_approval'
|
||||
email_attendees = False
|
||||
elif free_flow:
|
||||
email_template = request.event.settings.mail_text_order_free
|
||||
log_entry = 'pretix.event.order.email.order_free'
|
||||
email_attendees = request.event.settings.mail_send_order_free_attendee
|
||||
@@ -652,12 +663,13 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
|
||||
_order_placed_email(
|
||||
request.event, order, payment.payment_provider if payment else None, email_template,
|
||||
log_entry, invoice, payment
|
||||
log_entry, invoice, payment, is_free=free_flow
|
||||
)
|
||||
if email_attendees:
|
||||
for p in order.positions.all():
|
||||
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
|
||||
_order_placed_email_attendee(request.event, order, p, email_attendees_template, log_entry)
|
||||
_order_placed_email_attendee(request.event, order, p, email_attendees_template, log_entry,
|
||||
is_free=free_flow)
|
||||
|
||||
if not free_flow and order.status == Order.STATUS_PAID and payment:
|
||||
payment._send_paid_mail(invoice, None, '')
|
||||
@@ -690,6 +702,16 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
}
|
||||
)
|
||||
|
||||
if 'custom_followup_at' in self.request.data and serializer.instance.custom_followup_at != self.request.data.get('custom_followup_at'):
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.order.custom_followup_at',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={
|
||||
'new_custom_followup_at': self.request.data.get('custom_followup_at')
|
||||
}
|
||||
)
|
||||
|
||||
if 'checkin_attention' in self.request.data and serializer.instance.checkin_attention != self.request.data.get('checkin_attention'):
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.order.checkin_attention',
|
||||
@@ -781,7 +803,7 @@ with scopes_disabled():
|
||||
)
|
||||
|
||||
def has_checkin_qs(self, queryset, name, value):
|
||||
return queryset.filter(checkins__isnull=not value)
|
||||
return queryset.alias(ce=Exists(Checkin.objects.filter(position=OuterRef('pk')))).filter(ce=value)
|
||||
|
||||
def attendee_name_qs(self, queryset, name, value):
|
||||
return queryset.filter(Q(attendee_name_cached__iexact=value) | Q(addon_to__attendee_name_cached__iexact=value))
|
||||
@@ -835,7 +857,8 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, mixins.UpdateModelMixin, vi
|
||||
qs = qs.filter(order__event=self.request.event)
|
||||
if self.request.query_params.get('pdf_data', 'false') == 'true':
|
||||
qs = qs.prefetch_related(
|
||||
'checkins', 'answers', 'answers__options', 'answers__question',
|
||||
Prefetch('checkins', queryset=Checkin.objects.all()),
|
||||
'answers', 'answers__options', 'answers__question',
|
||||
Prefetch('addons', qs.select_related('item', 'variation')),
|
||||
Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related(
|
||||
Prefetch(
|
||||
@@ -845,7 +868,8 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, mixins.UpdateModelMixin, vi
|
||||
Prefetch(
|
||||
'positions',
|
||||
qs.prefetch_related(
|
||||
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
||||
'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
||||
Prefetch('checkins', queryset=Checkin.objects.all()),
|
||||
)
|
||||
)
|
||||
))
|
||||
@@ -854,7 +878,8 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, mixins.UpdateModelMixin, vi
|
||||
)
|
||||
else:
|
||||
qs = qs.prefetch_related(
|
||||
'checkins', 'answers', 'answers__options', 'answers__question',
|
||||
Prefetch('checkins', queryset=Checkin.objects.all()),
|
||||
'answers', 'answers__options', 'answers__question',
|
||||
).select_related(
|
||||
'item', 'order', 'order__event', 'order__event__organizer', 'seat'
|
||||
)
|
||||
@@ -1436,8 +1461,14 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
inv = self.get_object()
|
||||
if inv.canceled:
|
||||
raise ValidationError('The invoice has already been canceled.')
|
||||
if not inv.event.settings.invoice_regenerate_allowed:
|
||||
raise PermissionDenied('Invoices may not be changed after they are created.')
|
||||
elif inv.shredded:
|
||||
raise PermissionDenied('The invoice file is no longer stored on the server.')
|
||||
elif inv.sent_to_organizer:
|
||||
raise PermissionDenied('The invoice file has already been exported.')
|
||||
elif now().astimezone(self.request.event.timezone).date() - inv.date > datetime.timedelta(days=1):
|
||||
raise PermissionDenied('The invoice file is too old to be regenerated.')
|
||||
else:
|
||||
inv = regenerate_invoice(inv)
|
||||
inv.order.log_action(
|
||||
|
||||
@@ -261,7 +261,7 @@ def register_default_webhook_events(sender, **kwargs):
|
||||
),
|
||||
ParametrizedEventWebhookEvent(
|
||||
'pretix.event.deleted',
|
||||
_('Event details changed'),
|
||||
_('Event deleted'),
|
||||
),
|
||||
ParametrizedSubEventWebhookEvent(
|
||||
'pretix.subevent.added',
|
||||
|
||||
@@ -47,6 +47,7 @@ class PretixBaseConfig(AppConfig):
|
||||
from . import notifications # NOQA
|
||||
from . import email # NOQA
|
||||
from .services import auth, checkin, export, mail, tickets, cart, orderimport, orders, invoices, cleanup, update_check, quotas, notifications, vouchers # NOQA
|
||||
from .models import _transactions # NOQA
|
||||
from django.conf import settings
|
||||
|
||||
try:
|
||||
|
||||
@@ -94,6 +94,9 @@ class BaseAuthBackend:
|
||||
This method will be called after the user filled in the login form. ``request`` will contain
|
||||
the current request and ``form_data`` the input for the form fields defined in ``login_form_fields``.
|
||||
You are expected to either return a ``User`` object (if login was successful) or ``None``.
|
||||
|
||||
You are expected to either return a ``User`` object (if login was successful) or ``None``. You should
|
||||
obtain this user object using ``User.objects.get_or_create_for_backend``.
|
||||
"""
|
||||
return
|
||||
|
||||
@@ -104,7 +107,9 @@ class BaseAuthBackend:
|
||||
reverse proxy, you can directly return a ``User`` object that will be logged in.
|
||||
|
||||
``request`` will contain the current request.
|
||||
You are expected to either return a ``User`` object (if login was successful) or ``None``.
|
||||
|
||||
You are expected to either return a ``User`` object (if login was successful) or ``None``. You should
|
||||
obtain this user object using ``User.objects.get_or_create_for_backend``.
|
||||
"""
|
||||
return
|
||||
|
||||
@@ -146,7 +151,8 @@ class NativeAuthBackend(BaseAuthBackend):
|
||||
d = OrderedDict([
|
||||
('email', forms.EmailField(label=_("E-mail"), max_length=254,
|
||||
widget=forms.EmailInput(attrs={'autofocus': 'autofocus'}))),
|
||||
('password', forms.CharField(label=_("Password"), widget=forms.PasswordInput)),
|
||||
('password', forms.CharField(label=_("Password"), widget=forms.PasswordInput,
|
||||
max_length=4096)),
|
||||
])
|
||||
return d
|
||||
|
||||
|
||||
@@ -82,6 +82,13 @@ class SalesChannel:
|
||||
"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def customer_accounts_supported(self) -> bool:
|
||||
"""
|
||||
If this property is ``True``, checkout will show the customer login step.
|
||||
"""
|
||||
return True
|
||||
|
||||
|
||||
def get_all_sales_channels():
|
||||
global _ALL_CHANNELS
|
||||
|
||||
@@ -30,7 +30,7 @@ from pretix.base.settings import GlobalSettingsObject
|
||||
from pretix.base.templatetags.safelink import safelink as sl
|
||||
|
||||
|
||||
def get_powered_by(safelink=True):
|
||||
def get_powered_by(request, safelink=True):
|
||||
gs = GlobalSettingsObject()
|
||||
d = gs.settings.license_check_input
|
||||
if d.get('poweredby_name'):
|
||||
@@ -57,7 +57,7 @@ def get_powered_by(safelink=True):
|
||||
|
||||
if d.get('base_license') == 'agpl':
|
||||
msg += ' (<a href="{}" target="_blank" rel="noopener">{}</a>)'.format(
|
||||
reverse('source'),
|
||||
request.build_absolute_uri(reverse('source')),
|
||||
gettext('source code')
|
||||
)
|
||||
|
||||
@@ -69,7 +69,7 @@ def contextprocessor(request):
|
||||
'rtl': getattr(request, 'LANGUAGE_CODE', 'en') in settings.LANGUAGES_RTL,
|
||||
}
|
||||
try:
|
||||
ctx['poweredby'] = get_powered_by(safelink=True)
|
||||
ctx['poweredby'] = get_powered_by(request, safelink=True)
|
||||
except Exception:
|
||||
ctx['poweredby'] = 'powered by <a href="https://pretix.eu/" target="_blank" rel="noopener">pretix</a>'
|
||||
if settings.DEBUG and 'runserver' not in sys.argv:
|
||||
|
||||
@@ -25,17 +25,19 @@ from datetime import timedelta
|
||||
from decimal import Decimal
|
||||
from itertools import groupby
|
||||
from smtplib import SMTPResponseException
|
||||
from typing import TypeVar
|
||||
|
||||
import css_inline
|
||||
from django.conf import settings
|
||||
from django.core.mail.backends.smtp import EmailBackend
|
||||
from django.db.models import Count
|
||||
from django.dispatch import receiver
|
||||
from django.template.loader import get_template
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import (
|
||||
get_language, gettext_lazy as _, pgettext_lazy,
|
||||
)
|
||||
from inlinestyler.utils import inline_css
|
||||
|
||||
from pretix.base.i18n import (
|
||||
LazyCurrencyNumber, LazyDate, LazyExpiresDate, LazyNumber,
|
||||
@@ -49,23 +51,23 @@ from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||
|
||||
logger = logging.getLogger('pretix.base.email')
|
||||
|
||||
T = TypeVar("T", bound=EmailBackend)
|
||||
|
||||
class CustomSMTPBackend(EmailBackend):
|
||||
|
||||
def test(self, from_addr):
|
||||
try:
|
||||
self.open()
|
||||
self.connection.ehlo_or_helo_if_needed()
|
||||
(code, resp) = self.connection.mail(from_addr, [])
|
||||
if code != 250:
|
||||
logger.warn('Error testing mail settings, code %d, resp: %s' % (code, resp))
|
||||
raise SMTPResponseException(code, resp)
|
||||
(code, resp) = self.connection.rcpt('testdummy@pretix.eu')
|
||||
if (code != 250) and (code != 251):
|
||||
logger.warn('Error testing mail settings, code %d, resp: %s' % (code, resp))
|
||||
raise SMTPResponseException(code, resp)
|
||||
finally:
|
||||
self.close()
|
||||
def test_custom_smtp_backend(backend: T, from_addr: str) -> None:
|
||||
try:
|
||||
backend.open()
|
||||
backend.connection.ehlo_or_helo_if_needed()
|
||||
(code, resp) = backend.connection.mail(from_addr, [])
|
||||
if code != 250:
|
||||
logger.warning('Error testing mail settings, code %d, resp: %s' % (code, resp))
|
||||
raise SMTPResponseException(code, resp)
|
||||
(code, resp) = backend.connection.rcpt('testdummy@pretix.eu')
|
||||
if (code != 250) and (code != 251):
|
||||
logger.warning('Error testing mail settings, code %d, resp: %s' % (code, resp))
|
||||
raise SMTPResponseException(code, resp)
|
||||
finally:
|
||||
backend.close()
|
||||
|
||||
|
||||
class BaseHTMLMailRenderer:
|
||||
@@ -163,9 +165,20 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
||||
has_addons=Count('addons')
|
||||
))
|
||||
htmlctx['cart'] = [(k, list(v)) for k, v in groupby(
|
||||
positions, key=lambda op: (
|
||||
op.item, op.variation, op.subevent, op.attendee_name,
|
||||
(op.pk if op.addon_to_id else None), (op.pk if op.has_addons else None)
|
||||
sorted(
|
||||
positions,
|
||||
key=lambda op: (
|
||||
(op.addon_to.positionid if op.addon_to_id else op.positionid),
|
||||
op.positionid
|
||||
)
|
||||
),
|
||||
key=lambda op: (
|
||||
op.item,
|
||||
op.variation,
|
||||
op.subevent,
|
||||
op.attendee_name,
|
||||
op.addon_to_id,
|
||||
(op.pk if op.has_addons else None)
|
||||
)
|
||||
)]
|
||||
|
||||
@@ -174,7 +187,11 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
||||
htmlctx['ev'] = position.subevent or self.event
|
||||
|
||||
tpl = get_template(self.template_name)
|
||||
body_html = inline_css(tpl.render(htmlctx))
|
||||
body_html = tpl.render(htmlctx)
|
||||
|
||||
inliner = css_inline.CSSInliner(remove_style_tags=True)
|
||||
body_html = inliner.inline(body_html)
|
||||
|
||||
return body_html
|
||||
|
||||
|
||||
@@ -293,7 +310,11 @@ def get_email_context(**kwargs):
|
||||
val = [val]
|
||||
for v in val:
|
||||
if all(rp in kwargs for rp in v.required_context):
|
||||
ctx[v.identifier] = v.render(kwargs)
|
||||
try:
|
||||
ctx[v.identifier] = v.render(kwargs)
|
||||
except:
|
||||
ctx[v.identifier] = '(error)'
|
||||
logger.exception(f'Failed to process email placeholder {v.identifier}.')
|
||||
return ctx
|
||||
|
||||
|
||||
@@ -448,6 +469,35 @@ def base_placeholders(sender, **kwargs):
|
||||
}
|
||||
),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'event_location', ['event_or_subevent'], lambda event_or_subevent: str(event_or_subevent.location or ''),
|
||||
lambda event: str(event.location or ''),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'event_admission_time', ['event_or_subevent'],
|
||||
lambda event_or_subevent: date_format(event_or_subevent.date_admission, 'TIME_FORMAT') if event_or_subevent.date_admission else '',
|
||||
lambda event: date_format(event.date_admission, 'TIME_FORMAT') if event.date_admission else '',
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'subevent', ['waiting_list_entry', 'event'],
|
||||
lambda waiting_list_entry, event: str(waiting_list_entry.subevent or event),
|
||||
lambda event: str(event if not event.has_subevents or not event.subevents.exists() else event.subevents.first())
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'subevent_date_from', ['waiting_list_entry', 'event'],
|
||||
lambda waiting_list_entry, event: (waiting_list_entry.subevent or event).get_date_from_display(),
|
||||
lambda event: (event if not event.has_subevents or not event.subevents.exists() else event.subevents.first()).get_date_from_display()
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'url_remove', ['waiting_list_entry', 'event'],
|
||||
lambda waiting_list_entry, event: build_absolute_uri(
|
||||
event, 'presale:event.waitinglist.remove'
|
||||
) + '?voucher=' + waiting_list_entry.voucher.code,
|
||||
lambda event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.waitinglist.remove',
|
||||
) + '?voucher=68CYU2H6ZTP3WLK5',
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'url', ['waiting_list_entry', 'event'],
|
||||
lambda waiting_list_entry, event: build_absolute_uri(
|
||||
@@ -515,6 +565,22 @@ def base_placeholders(sender, **kwargs):
|
||||
'voucher_list', ['voucher_list'], lambda voucher_list: ' \n'.join(voucher_list),
|
||||
' 68CYU2H6ZTP3WLK5\n 7MB94KKPVEPSMVF2'
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
|
||||
'voucher_url_list', ['event', 'voucher_list'],
|
||||
lambda event, voucher_list: ' \n'.join([
|
||||
build_absolute_uri(
|
||||
event, 'presale:event.redeem'
|
||||
) + '?voucher=' + c
|
||||
for c in voucher_list
|
||||
]),
|
||||
lambda event: ' \n'.join([
|
||||
build_absolute_uri(
|
||||
event, 'presale:event.redeem'
|
||||
) + '?voucher=' + c
|
||||
for c in ['68CYU2H6ZTP3WLK5', '7MB94KKPVEPSMVF2']
|
||||
]),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'url', ['event', 'voucher_list'], lambda event, voucher_list: build_absolute_uri(event, 'presale:event.index', kwargs={
|
||||
'event': event.slug,
|
||||
@@ -581,6 +647,10 @@ def base_placeholders(sender, **kwargs):
|
||||
'meta_%s' % k, ['event'], lambda event, k=k: event.meta_data[k],
|
||||
v
|
||||
))
|
||||
ph.append(SimpleFunctionalMailTextPlaceholder(
|
||||
'meta_%s' % k, ['event_or_subevent'], lambda event_or_subevent, k=k: event_or_subevent.meta_data[k],
|
||||
v
|
||||
))
|
||||
|
||||
return ph
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import io
|
||||
import re
|
||||
import tempfile
|
||||
from collections import OrderedDict, namedtuple
|
||||
from decimal import Decimal
|
||||
@@ -46,23 +45,13 @@ from django.conf import settings
|
||||
from django.db.models import QuerySet
|
||||
from django.utils.formats import localize
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE, KNOWN_TYPES
|
||||
|
||||
from pretix.base.models import Event
|
||||
from pretix.helpers.safe_openpyxl import ( # NOQA: backwards compatibility for plugins using excel_safe
|
||||
SafeWorkbook, remove_invalid_excel_chars as excel_safe,
|
||||
)
|
||||
|
||||
|
||||
def excel_safe(val):
|
||||
if not isinstance(val, KNOWN_TYPES):
|
||||
val = str(val)
|
||||
|
||||
if isinstance(val, bytes):
|
||||
val = val.decode("utf-8", errors="ignore")
|
||||
|
||||
if isinstance(val, str):
|
||||
val = re.sub(ILLEGAL_CHARACTERS_RE, '', val)
|
||||
|
||||
return val
|
||||
__ = excel_safe # just so the compatbility import above is "used" and doesn't get removed by linter
|
||||
|
||||
|
||||
class BaseExporter:
|
||||
@@ -70,8 +59,9 @@ class BaseExporter:
|
||||
This is the base class for all data exporters
|
||||
"""
|
||||
|
||||
def __init__(self, event, progress_callback=lambda v: None):
|
||||
def __init__(self, event, organizer, progress_callback=lambda v: None):
|
||||
self.event = event
|
||||
self.organizer = organizer
|
||||
self.progress_callback = progress_callback
|
||||
self.is_multievent = isinstance(event, QuerySet)
|
||||
if isinstance(event, QuerySet):
|
||||
@@ -220,9 +210,13 @@ class ListExporter(BaseExporter):
|
||||
writer.writerow(line)
|
||||
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
|
||||
|
||||
def prepare_xlsx_sheet(self, ws):
|
||||
pass
|
||||
|
||||
def _render_xlsx(self, form_data, output_file=None):
|
||||
wb = Workbook(write_only=True)
|
||||
wb = SafeWorkbook(write_only=True)
|
||||
ws = wb.create_sheet()
|
||||
self.prepare_xlsx_sheet(ws)
|
||||
try:
|
||||
ws.title = str(self.verbose_name)
|
||||
except:
|
||||
@@ -234,7 +228,7 @@ class ListExporter(BaseExporter):
|
||||
total = line.total
|
||||
continue
|
||||
ws.append([
|
||||
excel_safe(val) for val in line
|
||||
val for val in line
|
||||
])
|
||||
if total:
|
||||
counter += 1
|
||||
@@ -339,7 +333,7 @@ class MultiSheetListExporter(ListExporter):
|
||||
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
|
||||
|
||||
def _render_xlsx(self, form_data, output_file=None):
|
||||
wb = Workbook(write_only=True)
|
||||
wb = SafeWorkbook(write_only=True)
|
||||
n_sheets = len(self.sheets)
|
||||
for i_sheet, (s, l) in enumerate(self.sheets):
|
||||
ws = wb.create_sheet(str(l))
|
||||
@@ -353,8 +347,7 @@ class MultiSheetListExporter(ListExporter):
|
||||
total = line.total
|
||||
continue
|
||||
ws.append([
|
||||
excel_safe(val)
|
||||
for val in line
|
||||
val for val in line
|
||||
])
|
||||
if total:
|
||||
counter += 1
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#
|
||||
from .answers import * # noqa
|
||||
from .dekodi import * # noqa
|
||||
from .events import * # noqa
|
||||
from .invoices import * # noqa
|
||||
from .json import * # noqa
|
||||
from .mail import * # noqa
|
||||
|
||||
100
src/pretix/base/exporters/events.py
Normal file
100
src/pretix/base/exporters/events.py
Normal file
@@ -0,0 +1,100 @@
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
|
||||
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
#
|
||||
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
|
||||
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
|
||||
#
|
||||
# This file contains Apache-licensed contributions copyrighted by: Tobias Kunze
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from ...control.forms.filter import get_all_payment_providers
|
||||
from ..exporter import ListExporter
|
||||
from ..signals import register_multievent_data_exporters
|
||||
|
||||
|
||||
class EventDataExporter(ListExporter):
|
||||
identifier = 'eventdata'
|
||||
verbose_name = _('Event data')
|
||||
|
||||
@cached_property
|
||||
def providers(self):
|
||||
return dict(get_all_payment_providers())
|
||||
|
||||
def iterate_list(self, form_data):
|
||||
header = [
|
||||
_("Event name"),
|
||||
_("Short form"),
|
||||
_("Shop is live"),
|
||||
_("Event currency"),
|
||||
_("Event start time"),
|
||||
_("Event end time"),
|
||||
_("Admission time"),
|
||||
_("Start of presale"),
|
||||
_("End of presale"),
|
||||
_("Location"),
|
||||
_("Latitude"),
|
||||
_("Longitude"),
|
||||
_("Internal comment"),
|
||||
]
|
||||
props = list(self.organizer.meta_properties.all())
|
||||
for p in props:
|
||||
header.append(p.name)
|
||||
yield header
|
||||
|
||||
for e in self.events.all():
|
||||
m = e.meta_data
|
||||
yield [
|
||||
str(e.name),
|
||||
e.slug,
|
||||
_('Yes') if e.live else _('No'),
|
||||
e.currency,
|
||||
date_format(e.date_from, 'SHORT_DATETIME_FORMAT'),
|
||||
date_format(e.date_to, 'SHORT_DATETIME_FORMAT') if e.date_to else '',
|
||||
date_format(e.date_admission, 'SHORT_DATETIME_FORMAT') if e.date_admission else '',
|
||||
date_format(e.presale_start, 'SHORT_DATETIME_FORMAT') if e.presale_start else '',
|
||||
date_format(e.presale_end, 'SHORT_DATETIME_FORMAT') if e.presale_end else '',
|
||||
str(e.location),
|
||||
e.geo_lat or '',
|
||||
e.geo_lon or '',
|
||||
e.comment,
|
||||
] + [
|
||||
m.get(p.name, '') for p in props
|
||||
]
|
||||
|
||||
def get_filename(self):
|
||||
return '{}_events'.format(self.events.first().organizer.slug)
|
||||
|
||||
|
||||
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_eventdata")
|
||||
def register_multievent_eventdata_exporter(sender, **kwargs):
|
||||
return EventDataExporter
|
||||
@@ -324,7 +324,6 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
|
||||
_('Tax rate'),
|
||||
_('Tax name'),
|
||||
_('Event start date'),
|
||||
|
||||
_('Date'),
|
||||
_('Order code'),
|
||||
_('E-mail address'),
|
||||
@@ -348,6 +347,8 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
|
||||
_('Invoice recipient:') + ' ' + _('Beneficiary'),
|
||||
_('Invoice recipient:') + ' ' + _('Internal reference'),
|
||||
_('Payment providers'),
|
||||
_('Event end date'),
|
||||
_('Location'),
|
||||
]
|
||||
|
||||
p_providers = OrderPayment.objects.filter(
|
||||
@@ -406,7 +407,9 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
|
||||
', '.join([
|
||||
str(self.providers.get(p, p)) for p in sorted(set((l.payment_providers or '').split(',')))
|
||||
if p and p != 'free'
|
||||
])
|
||||
]),
|
||||
date_format(l.event_date_to, "SHORT_DATE_FORMAT") if l.event_date_to else "",
|
||||
l.event_location or "",
|
||||
]
|
||||
|
||||
@cached_property
|
||||
|
||||
@@ -55,16 +55,20 @@ class JSONExporter(BaseExporter):
|
||||
'name': str(self.event.organizer.name),
|
||||
'slug': self.event.organizer.slug
|
||||
},
|
||||
'meta_data': self.event.meta_data,
|
||||
'categories': [
|
||||
{
|
||||
'id': category.id,
|
||||
'name': str(category.name),
|
||||
'description': str(category.description),
|
||||
'position': category.position,
|
||||
'internal_name': category.internal_name
|
||||
} for category in self.event.categories.all()
|
||||
],
|
||||
'items': [
|
||||
{
|
||||
'id': item.id,
|
||||
'position': item.position,
|
||||
'name': str(item.name),
|
||||
'internal_name': str(item.internal_name),
|
||||
'category': item.category_id,
|
||||
@@ -73,13 +77,35 @@ class JSONExporter(BaseExporter):
|
||||
'tax_name': str(item.tax_rule.name) if item.tax_rule else None,
|
||||
'admission': item.admission,
|
||||
'active': item.active,
|
||||
'sales_channels': item.sales_channels,
|
||||
'description': str(item.description),
|
||||
'available_from': item.available_from,
|
||||
'available_until': item.available_until,
|
||||
'require_voucher': item.require_voucher,
|
||||
'hide_without_voucher': item.hide_without_voucher,
|
||||
'allow_cancel': item.allow_cancel,
|
||||
'require_bundling': item.require_bundling,
|
||||
'min_per_order': item.min_per_order,
|
||||
'max_per_order': item.max_per_order,
|
||||
'checkin_attention': item.checkin_attention,
|
||||
'original_price': item.original_price,
|
||||
'issue_giftcard': item.issue_giftcard,
|
||||
'meta_data': item.meta_data,
|
||||
'require_membership': item.require_membership,
|
||||
'variations': [
|
||||
{
|
||||
'id': variation.id,
|
||||
'active': variation.active,
|
||||
'price': variation.default_price if variation.default_price is not None else
|
||||
item.default_price,
|
||||
'name': str(variation)
|
||||
'name': str(variation),
|
||||
'description': str(variation.description),
|
||||
'position': variation.position,
|
||||
'require_membership': variation.require_membership,
|
||||
'sales_channels': variation.sales_channels,
|
||||
'available_from': variation.available_from,
|
||||
'available_until': variation.available_until,
|
||||
'hide_without_voucher': variation.hide_without_voucher,
|
||||
} for variation in item.variations.all()
|
||||
]
|
||||
} for item in self.event.items.select_related('tax_rule').prefetch_related('variations')
|
||||
@@ -87,7 +113,13 @@ class JSONExporter(BaseExporter):
|
||||
'questions': [
|
||||
{
|
||||
'id': question.id,
|
||||
'identifier': question.identifier,
|
||||
'required': question.required,
|
||||
'question': str(question.question),
|
||||
'position': question.position,
|
||||
'hidden': question.hidden,
|
||||
'ask_during_checkin': question.ask_during_checkin,
|
||||
'help_text': str(question.help_text),
|
||||
'type': question.type
|
||||
} for question in self.event.questions.all()
|
||||
],
|
||||
@@ -95,7 +127,18 @@ class JSONExporter(BaseExporter):
|
||||
{
|
||||
'code': order.code,
|
||||
'status': order.status,
|
||||
'customer': order.customer.identifier if order.customer else None,
|
||||
'testmode': order.testmode,
|
||||
'user': order.email,
|
||||
'email': order.email,
|
||||
'phone': str(order.phone),
|
||||
'locale': order.locale,
|
||||
'comment': order.comment,
|
||||
'custom_followup_at': order.custom_followup_at,
|
||||
'require_approval': order.require_approval,
|
||||
'checkin_attention': order.checkin_attention,
|
||||
'sales_channel': order.sales_channel,
|
||||
'expires': order.expires,
|
||||
'datetime': order.datetime,
|
||||
'fees': [
|
||||
{
|
||||
@@ -108,11 +151,21 @@ class JSONExporter(BaseExporter):
|
||||
'positions': [
|
||||
{
|
||||
'id': position.id,
|
||||
'positionid': position.positionid,
|
||||
'item': position.item_id,
|
||||
'variation': position.variation_id,
|
||||
'subevent': position.subevent_id,
|
||||
'seat': position.seat.seat_guid if position.seat else None,
|
||||
'price': position.price,
|
||||
'tax_rate': position.tax_rate,
|
||||
'tax_value': position.tax_value,
|
||||
'attendee_name': position.attendee_name,
|
||||
'attendee_email': position.attendee_email,
|
||||
'company': position.company,
|
||||
'street': position.street,
|
||||
'zipcode': position.zipcode,
|
||||
'country': str(position.country) if position.country else None,
|
||||
'state': position.state,
|
||||
'secret': position.secret,
|
||||
'addon_to': position.addon_to_id,
|
||||
'answers': [
|
||||
@@ -124,15 +177,30 @@ class JSONExporter(BaseExporter):
|
||||
} for position in order.positions.all()
|
||||
]
|
||||
} for order in
|
||||
self.event.orders.all().prefetch_related('positions', 'positions__answers', 'fees')
|
||||
self.event.orders.all().prefetch_related('positions', 'positions__answers', 'positions__seat', 'customer', 'fees')
|
||||
],
|
||||
'quotas': [
|
||||
{
|
||||
'id': quota.id,
|
||||
'size': quota.size,
|
||||
'subevent': quota.subevent_id,
|
||||
'items': [item.id for item in quota.items.all()],
|
||||
'variations': [variation.id for variation in quota.variations.all()],
|
||||
} for quota in self.event.quotas.all().prefetch_related('items', 'variations')
|
||||
],
|
||||
'subevents': [
|
||||
{
|
||||
'id': se.id,
|
||||
'name': str(se.name),
|
||||
'location': str(se.location),
|
||||
'date_from': se.date_from,
|
||||
'date_to': se.date_to,
|
||||
'date_admission': se.date_admission,
|
||||
'geo_lat': se.geo_lat,
|
||||
'geo_lon': se.geo_lon,
|
||||
'is_public': se.is_public,
|
||||
'meta_data': se.meta_data,
|
||||
} for se in self.event.subevents.all()
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
from collections import OrderedDict
|
||||
from datetime import date, datetime, time
|
||||
from decimal import Decimal
|
||||
|
||||
import dateutil
|
||||
@@ -42,10 +43,10 @@ from django.db.models import (
|
||||
Case, CharField, Count, DateTimeField, F, IntegerField, Max, Min, OuterRef,
|
||||
Q, Subquery, Sum, When,
|
||||
)
|
||||
from django.db.models.functions import Coalesce, TruncDate
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.dispatch import receiver
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import get_current_timezone, now
|
||||
from django.utils.timezone import get_current_timezone, make_aware, now
|
||||
from django.utils.translation import gettext as _, gettext_lazy, pgettext
|
||||
|
||||
from pretix.base.models import (
|
||||
@@ -129,7 +130,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
label=_('End event date'),
|
||||
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
|
||||
required=False,
|
||||
help_text=_('Only include orders including at least one ticket for a date on or after this date. '
|
||||
help_text=_('Only include orders including at least one ticket for a date on or before this date. '
|
||||
'Will also include other dates in case of mixed orders!')
|
||||
)),
|
||||
]
|
||||
@@ -181,41 +182,43 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
|
||||
if form_data.get('date_from'):
|
||||
date_value = form_data.get('date_from')
|
||||
if isinstance(date_value, str):
|
||||
if not isinstance(date_value, date):
|
||||
date_value = dateutil.parser.parse(date_value).date()
|
||||
datetime_value = make_aware(datetime.combine(date_value, time(0, 0, 0)), self.timezone)
|
||||
|
||||
annotations['date'] = TruncDate(f'{rel}datetime')
|
||||
filters['date__gte'] = date_value
|
||||
filters[f'{rel}datetime__gte'] = datetime_value
|
||||
|
||||
if form_data.get('date_to'):
|
||||
date_value = form_data.get('date_to')
|
||||
if isinstance(date_value, str):
|
||||
if not isinstance(date_value, date):
|
||||
date_value = dateutil.parser.parse(date_value).date()
|
||||
datetime_value = make_aware(datetime.combine(date_value, time(23, 59, 59, 999999)), self.timezone)
|
||||
|
||||
annotations['date'] = TruncDate(f'{rel}datetime')
|
||||
filters['date__lte'] = date_value
|
||||
filters[f'{rel}datetime__lte'] = datetime_value
|
||||
|
||||
if form_data.get('event_date_from'):
|
||||
date_value = form_data.get('event_date_from')
|
||||
if isinstance(date_value, str):
|
||||
if not isinstance(date_value, date):
|
||||
date_value = dateutil.parser.parse(date_value).date()
|
||||
datetime_value = make_aware(datetime.combine(date_value, time(0, 0, 0)), self.timezone)
|
||||
|
||||
annotations['event_date_max'] = Case(
|
||||
When(**{f'{rel}event__has_subevents': True}, then=Max(f'{rel}all_positions__subevent__date_from')),
|
||||
default=F(f'{rel}event__date_from'),
|
||||
)
|
||||
filters['event_date_max__gte'] = date_value
|
||||
filters['event_date_max__gte'] = datetime_value
|
||||
|
||||
if form_data.get('event_date_to'):
|
||||
date_value = form_data.get('event_date_to')
|
||||
if isinstance(date_value, str):
|
||||
if not isinstance(date_value, date):
|
||||
date_value = dateutil.parser.parse(date_value).date()
|
||||
datetime_value = make_aware(datetime.combine(date_value, time(23, 59, 59, 999999)), self.timezone)
|
||||
|
||||
annotations['event_date_min'] = Case(
|
||||
When(**{f'{rel}event__has_subevents': True}, then=Min(f'{rel}all_positions__subevent__date_from')),
|
||||
default=F(f'{rel}event__date_from'),
|
||||
)
|
||||
filters['event_date_min__lte'] = date_value
|
||||
filters['event_date_min__lte'] = datetime_value
|
||||
|
||||
if filters:
|
||||
return qs.annotate(**annotations).filter(**filters)
|
||||
@@ -288,6 +291,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
headers.append(_('Sales channel'))
|
||||
headers.append(_('Requires special attention'))
|
||||
headers.append(_('Comment'))
|
||||
headers.append(_('Follow-up date'))
|
||||
headers.append(_('Positions'))
|
||||
headers.append(_('E-mail address verified'))
|
||||
headers.append(_('Payment providers'))
|
||||
@@ -393,6 +397,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
row.append(order.sales_channel)
|
||||
row.append(_('Yes') if order.checkin_attention else _('No'))
|
||||
row.append(order.comment or "")
|
||||
row.append(order.custom_followup_at.strftime("%Y-%m-%d") if order.custom_followup_at else "")
|
||||
row.append(order.pcnt)
|
||||
row.append(_('Yes') if order.email_known_to_work else _('No'))
|
||||
row.append(', '.join([
|
||||
@@ -568,12 +573,14 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
pgettext('address', 'State'),
|
||||
_('Voucher'),
|
||||
_('Pseudonymization ID'),
|
||||
_('Ticket secret'),
|
||||
_('Seat ID'),
|
||||
_('Seat name'),
|
||||
_('Seat zone'),
|
||||
_('Seat row'),
|
||||
_('Seat number'),
|
||||
_('Order comment'),
|
||||
_('Follow-up date'),
|
||||
]
|
||||
|
||||
questions = list(Question.objects.filter(event__in=self.events))
|
||||
@@ -663,6 +670,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
op.state or '',
|
||||
op.voucher.code if op.voucher else '',
|
||||
op.pseudonymization_id,
|
||||
op.secret,
|
||||
]
|
||||
|
||||
if op.seat:
|
||||
@@ -677,6 +685,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
row += ['', '', '', '', '']
|
||||
|
||||
row.append(order.comment)
|
||||
row.append(order.custom_followup_at.strftime("%Y-%m-%d") if order.custom_followup_at else "")
|
||||
acache = {}
|
||||
for a in op.answers.all():
|
||||
# We do not want to localize Date, Time and Datetime question answers, as those can lead
|
||||
@@ -721,7 +730,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
row += [
|
||||
order.sales_channel,
|
||||
order.locale,
|
||||
row.append(_('Yes') if order.email_known_to_work else _('No'))
|
||||
_('Yes') if order.email_known_to_work else _('No')
|
||||
]
|
||||
row.append(', '.join([
|
||||
str(self.providers.get(p, p)) for p in sorted(set((op.payment_providers or '').split(',')))
|
||||
@@ -780,7 +789,7 @@ class PaymentListExporter(ListExporter):
|
||||
|
||||
headers = [
|
||||
_('Event slug'), _('Order'), _('Payment ID'), _('Creation date'), _('Completion date'), _('Status'),
|
||||
_('Status code'), _('Amount'), _('Payment method'), _('Comment')
|
||||
_('Status code'), _('Amount'), _('Payment method'), _('Comment'),
|
||||
]
|
||||
yield headers
|
||||
|
||||
@@ -866,6 +875,78 @@ class QuotaListExporter(ListExporter):
|
||||
return '{}_quotas'.format(self.event.slug)
|
||||
|
||||
|
||||
def generate_GiftCardTransactionListExporter(organizer): # hackhack
|
||||
class GiftcardTransactionListExporter(ListExporter):
|
||||
identifier = 'giftcardtransactionlist'
|
||||
verbose_name = gettext_lazy('Gift card transactions')
|
||||
|
||||
@property
|
||||
def additional_form_fields(self):
|
||||
d = [
|
||||
('date_from',
|
||||
forms.DateField(
|
||||
label=_('Start date'),
|
||||
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
|
||||
required=False,
|
||||
)),
|
||||
('date_to',
|
||||
forms.DateField(
|
||||
label=_('End date'),
|
||||
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
|
||||
required=False,
|
||||
)),
|
||||
]
|
||||
d = OrderedDict(d)
|
||||
return d
|
||||
|
||||
def iterate_list(self, form_data):
|
||||
qs = GiftCardTransaction.objects.filter(
|
||||
card__issuer=organizer,
|
||||
).order_by('datetime').select_related('card', 'order', 'order__event')
|
||||
|
||||
if form_data.get('date_from'):
|
||||
date_value = form_data.get('date_from')
|
||||
if isinstance(date_value, str):
|
||||
date_value = dateutil.parser.parse(date_value).date()
|
||||
qs = qs.filter(
|
||||
datetime__gte=make_aware(datetime.combine(date_value, time(0, 0, 0)), self.timezone)
|
||||
)
|
||||
|
||||
if form_data.get('date_to'):
|
||||
date_value = form_data.get('date_to')
|
||||
if isinstance(date_value, str):
|
||||
date_value = dateutil.parser.parse(date_value).date()
|
||||
|
||||
qs = qs.filter(
|
||||
datetime__lte=make_aware(datetime.combine(date_value, time(23, 59, 59, 999999)), self.timezone)
|
||||
)
|
||||
|
||||
headers = [
|
||||
_('Gift card code'),
|
||||
_('Test mode'),
|
||||
_('Date'),
|
||||
_('Amount'),
|
||||
_('Currency'),
|
||||
_('Order'),
|
||||
]
|
||||
yield headers
|
||||
|
||||
for obj in qs:
|
||||
row = [
|
||||
obj.card.secret,
|
||||
_('TEST MODE') if obj.card.testmode else '',
|
||||
obj.datetime.astimezone(self.timezone).strftime('%Y-%m-%d %H:%M:%S'),
|
||||
obj.value,
|
||||
obj.card.currency,
|
||||
obj.order.full_code if obj.order else None,
|
||||
]
|
||||
yield row
|
||||
|
||||
def get_filename(self):
|
||||
return '{}_giftcardtransactions'.format(organizer.slug)
|
||||
return GiftcardTransactionListExporter
|
||||
|
||||
|
||||
class GiftcardRedemptionListExporter(ListExporter):
|
||||
identifier = 'giftcardredemptionlist'
|
||||
verbose_name = gettext_lazy('Gift card redemptions')
|
||||
@@ -1058,3 +1139,8 @@ def register_multievent_i_giftcardredemptionlist_exporter(sender, **kwargs):
|
||||
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_giftcardlist")
|
||||
def register_multievent_i_giftcardlist_exporter(sender, **kwargs):
|
||||
return generate_GiftCardListExporter(sender)
|
||||
|
||||
|
||||
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_giftcardtransactionlist")
|
||||
def register_multievent_i_giftcardtransactionlist_exporter(sender, **kwargs):
|
||||
return generate_GiftCardTransactionListExporter(sender)
|
||||
|
||||
@@ -38,6 +38,7 @@ import i18nfield.forms
|
||||
from django import forms
|
||||
from django.forms.models import ModelFormMetaclass
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from formtools.wizard.views import SessionWizardView
|
||||
from hierarkey.forms import HierarkeyForm
|
||||
|
||||
@@ -112,12 +113,42 @@ class SettingsForm(i18nfield.forms.I18nFormMixin, HierarkeyForm):
|
||||
if isinstance(f, (RelativeDateTimeField, RelativeDateField)):
|
||||
f.set_event(self.obj)
|
||||
|
||||
def save(self):
|
||||
def _unmask_secret_fields(self):
|
||||
for k, v in self.cleaned_data.items():
|
||||
if isinstance(self.fields.get(k), SecretKeySettingsField) and self.cleaned_data.get(k) == SECRET_REDACTED:
|
||||
self.cleaned_data[k] = self.initial[k]
|
||||
|
||||
def save(self):
|
||||
self._unmask_secret_fields()
|
||||
return super().save()
|
||||
|
||||
def clean(self):
|
||||
d = super().clean()
|
||||
|
||||
# There is logic in HierarkeyForm.save() to only persist fields that changed. HierarkeyForm determines if
|
||||
# something changed by comparing `self._s.get(name)` to `value`. This leaves an edge case open for multi-lingual
|
||||
# text fields. On the very first load, the initial value in `self._s.get(name)` will be a LazyGettextProxy-based
|
||||
# string. However, only some of the languages are usually visible, so even if the user does not change anything
|
||||
# at all, it will be considered a changed value and stored. We do not want that, as it makes it very hard to add
|
||||
# languages to an organizer/event later on. So we trick it and make sure nothing gets changed in that situation.
|
||||
for name, field in self.fields.items():
|
||||
if isinstance(field, SecretKeySettingsField) and d.get(name) == SECRET_REDACTED and not self.initial.get(name):
|
||||
self.add_error(
|
||||
name,
|
||||
_('Due to technical reasons you cannot set inputs, that need to be masked (e.g. passwords), to %(value)s.') % {'value': SECRET_REDACTED}
|
||||
)
|
||||
|
||||
if isinstance(field, i18nfield.forms.I18nFormField):
|
||||
value = d.get(name)
|
||||
if not value:
|
||||
continue
|
||||
|
||||
current = self._s.get(name, as_type=type(value))
|
||||
if name not in self.changed_data:
|
||||
d[name] = current
|
||||
|
||||
return d
|
||||
|
||||
def get_new_filename(self, name: str) -> str:
|
||||
from pretix.base.models import Event
|
||||
|
||||
|
||||
@@ -154,6 +154,7 @@ class RegistrationForm(forms.Form):
|
||||
widget=forms.PasswordInput(attrs={
|
||||
'autocomplete': 'new-password' # see https://bugs.chromium.org/p/chromium/issues/detail?id=370363#c7
|
||||
}),
|
||||
max_length=4096,
|
||||
required=True
|
||||
)
|
||||
password_repeat = forms.CharField(
|
||||
@@ -161,6 +162,7 @@ class RegistrationForm(forms.Form):
|
||||
widget=forms.PasswordInput(attrs={
|
||||
'autocomplete': 'new-password' # see https://bugs.chromium.org/p/chromium/issues/detail?id=370363#c7
|
||||
}),
|
||||
max_length=4096,
|
||||
required=True
|
||||
)
|
||||
keep_logged_in = forms.BooleanField(label=_("Keep me logged in"), required=False)
|
||||
@@ -204,11 +206,13 @@ class PasswordRecoverForm(forms.Form):
|
||||
password = forms.CharField(
|
||||
label=_('Password'),
|
||||
widget=forms.PasswordInput,
|
||||
max_length=4096,
|
||||
required=True
|
||||
)
|
||||
password_repeat = forms.CharField(
|
||||
label=_('Repeat password'),
|
||||
widget=forms.PasswordInput
|
||||
widget=forms.PasswordInput,
|
||||
max_length=4096,
|
||||
)
|
||||
|
||||
def __init__(self, user_id=None, *args, **kwargs):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user