Compare commits
1376 Commits
a
...
widget-sea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b62737d4c | ||
|
|
d3698b3e2f | ||
|
|
ff828ecc92 | ||
|
|
d0236572f0 | ||
|
|
8ea6f3bc7d | ||
|
|
5587aebcd8 | ||
|
|
0b708067de | ||
|
|
e75dc74661 | ||
|
|
f0c5e54e34 | ||
|
|
eeb6e11934 | ||
|
|
1238165e7a | ||
|
|
bcf65603e4 | ||
|
|
c4aed04a18 | ||
|
|
8be09ee937 | ||
|
|
a1ec45daf6 | ||
|
|
9b0b8e2061 | ||
|
|
b6e65e7356 | ||
|
|
5d82305e18 | ||
|
|
c8983ca863 | ||
|
|
52f6b7c971 | ||
|
|
809177397a | ||
|
|
b83cb7d8c4 | ||
|
|
bfd980fc30 | ||
|
|
5bc3503d04 | ||
|
|
a582db3280 | ||
|
|
bd4ea5d8f8 | ||
|
|
5dec94606b | ||
|
|
ab97082c85 | ||
|
|
0723ff92ee | ||
|
|
15272cc3e6 | ||
|
|
60554dad9a | ||
|
|
b288ea1e96 | ||
|
|
6a4b792501 | ||
|
|
8dd83e5a35 | ||
|
|
bd5c9a4cb5 | ||
|
|
0cd8bbf9a9 | ||
|
|
d46989473b | ||
|
|
b31b2d34c0 | ||
|
|
5e963d87d9 | ||
|
|
a8e0eea69a | ||
|
|
efa9f6dfe5 | ||
|
|
857377d16c | ||
|
|
229b6fed4a | ||
|
|
b2e4fb6db3 | ||
|
|
16b15057fd | ||
|
|
e4168ff06a | ||
|
|
b208db32c7 | ||
|
|
ce177227c7 | ||
|
|
2cd70ef434 | ||
|
|
633755ab13 | ||
|
|
6ade32d7cb | ||
|
|
cea6c340be | ||
|
|
ad1dab3b7f | ||
|
|
930abe0cc5 | ||
|
|
ba2cc56c82 | ||
|
|
cb1f63bf80 | ||
|
|
aab7042cda | ||
|
|
495a21c683 | ||
|
|
86b5ba6937 | ||
|
|
3d9679a144 | ||
|
|
5f899ed5c5 | ||
|
|
47dabc1fe7 | ||
|
|
2d7c4a3d42 | ||
|
|
51ef98f736 | ||
|
|
2d7d2b1a90 | ||
|
|
cede7ba3aa | ||
|
|
4fd8726b05 | ||
|
|
b344ce90ba | ||
|
|
69dc7f56e5 | ||
|
|
247a61489f | ||
|
|
979d23e997 | ||
|
|
28e529995d | ||
|
|
a982cbf6b6 | ||
|
|
f1c2ae5b6b | ||
|
|
5b27ac66f9 | ||
|
|
c71ac2141f | ||
|
|
e59498d65d | ||
|
|
dfe3454915 | ||
|
|
b64c5735a8 | ||
|
|
11eecd739d | ||
|
|
07a6d4898a | ||
|
|
a759e23504 | ||
|
|
3eaf05502a | ||
|
|
04df1c2032 | ||
|
|
6a8df75a9f | ||
|
|
547cfdffd6 | ||
|
|
f72a0b4c09 | ||
|
|
3077292d15 | ||
|
|
2c831d5d6e | ||
|
|
be8d84be13 | ||
|
|
23c497e438 | ||
|
|
09643e47b9 | ||
|
|
1ef922cf56 | ||
|
|
b12ab02e89 | ||
|
|
cce98e0418 | ||
|
|
b8dd30b6dd | ||
|
|
ea9a96e124 | ||
|
|
b72dc0ce8e | ||
|
|
0a30fa70da | ||
|
|
add240a7b9 | ||
|
|
0b97198cff | ||
|
|
8f94d14479 | ||
|
|
0919d5dbca | ||
|
|
ff153164f8 | ||
|
|
b8e3d6c71d | ||
|
|
f782324d5f | ||
|
|
5259c8f33e | ||
|
|
079b72391c | ||
|
|
e9ba9a25df | ||
|
|
5858ed8d5c | ||
|
|
0b0ecf22bf | ||
|
|
3b1cd8e659 | ||
|
|
5e66809c7b | ||
|
|
c39328dd2a | ||
|
|
70ccd2fbe4 | ||
|
|
8c8e8031fc | ||
|
|
355b16e8e5 | ||
|
|
09c316ccba | ||
|
|
a1075840c6 | ||
|
|
b1a3ececad | ||
|
|
9624b1c505 | ||
|
|
d3589696d7 | ||
|
|
9523291651 | ||
|
|
b539f5e2f2 | ||
|
|
a18eb3be70 | ||
|
|
ac59bbff5d | ||
|
|
69f3e938f2 | ||
|
|
a0c1903ce5 | ||
|
|
3c8b188352 | ||
|
|
76e3b39f8f | ||
|
|
662e2cd116 | ||
|
|
eeaa3bc2a9 | ||
|
|
bbe8247606 | ||
|
|
5c46c1d14f | ||
|
|
651b676cfc | ||
|
|
5ee62c551e | ||
|
|
50e79b51de | ||
|
|
6e24c20a7a | ||
|
|
481a242054 | ||
|
|
f923c2fed0 | ||
|
|
228448b00f | ||
|
|
603345762a | ||
|
|
1812a23860 | ||
|
|
45374d0c94 | ||
|
|
c5f823596e | ||
|
|
eebb0a3527 | ||
|
|
bac1e8faf6 | ||
|
|
5cf7654099 | ||
|
|
988ef53972 | ||
|
|
36d20a45dd | ||
|
|
0691af7aa4 | ||
|
|
6b5436b71a | ||
|
|
a06a693c5c | ||
|
|
7b58ddbfde | ||
|
|
f18fb02d0b | ||
|
|
3a185b1cbc | ||
|
|
ba2a9fbd93 | ||
|
|
a337cf8efa | ||
|
|
616cc42b9c | ||
|
|
08012c42f2 | ||
|
|
08368684b0 | ||
|
|
17200df0cd | ||
|
|
28d1bedfc4 | ||
|
|
af90db9d1e | ||
|
|
19c4089da9 | ||
|
|
71723935e1 | ||
|
|
e2ad8f2f74 | ||
|
|
f8580a2789 | ||
|
|
cfeaa502a3 | ||
|
|
0ee8d6e9c3 | ||
|
|
a0e5717f7d | ||
|
|
49097037da | ||
|
|
62a6a11836 | ||
|
|
3d82058269 | ||
|
|
4f21bf8001 | ||
|
|
e32e7e2a50 | ||
|
|
5b8228bea0 | ||
|
|
a628f605a6 | ||
|
|
e658744f67 | ||
|
|
776c5e9fa2 | ||
|
|
46b5055aec | ||
|
|
ef227deb2e | ||
|
|
30cfe1ef3c | ||
|
|
4d5c828e2a | ||
|
|
f509306b35 | ||
|
|
706e479cff | ||
|
|
a5be7dcff5 | ||
|
|
845b3a866b | ||
|
|
91e1e079e1 | ||
|
|
9075c75a93 | ||
|
|
7b97204f2f | ||
|
|
dfedf09656 | ||
|
|
655cfe0afd | ||
|
|
faf17f824e | ||
|
|
fbf52a5219 | ||
|
|
9466c57c35 | ||
|
|
806ef8477b | ||
|
|
7cb654706a | ||
|
|
dea448e0f8 | ||
|
|
98b413249a | ||
|
|
4630c1fe8b | ||
|
|
bb718375e9 | ||
|
|
7d2dd722bd | ||
|
|
2adbd3cd4a | ||
|
|
fb483ad00e | ||
|
|
9cef65f359 | ||
|
|
ceeb69856b | ||
|
|
c184187e59 | ||
|
|
8ca38bdbaf | ||
|
|
3ae42b0c57 | ||
|
|
6368954ecb | ||
|
|
26ebdb7113 | ||
|
|
a1cb0b386b | ||
|
|
d46e1aba52 | ||
|
|
1f41184f9e | ||
|
|
2c746dffb2 | ||
|
|
84bd4e0e94 | ||
|
|
93f8b38745 | ||
|
|
4110d6ec15 | ||
|
|
9bea383ff0 | ||
|
|
2287c8b34c | ||
|
|
f7a129854e | ||
|
|
a96fccef63 | ||
|
|
dc5a85b39e | ||
|
|
23f9fb4a9a | ||
|
|
6130c45b3e | ||
|
|
83840c4024 | ||
|
|
02d1d1e0c3 | ||
|
|
f641f0fdd1 | ||
|
|
0c827c94a8 | ||
|
|
4fb76f1b55 | ||
|
|
cb3b1f3ac5 | ||
|
|
0b95f89882 | ||
|
|
bccd7cd1a4 | ||
|
|
9c33078a40 | ||
|
|
6403e5370a | ||
|
|
3fe2a0455f | ||
|
|
6956b198ae | ||
|
|
36f7a3d3a3 | ||
|
|
587e1a1c96 | ||
|
|
8707ab5277 | ||
|
|
4f6fa84fa7 | ||
|
|
e76d13bf8e | ||
|
|
39449ecbbe | ||
|
|
0204b42587 | ||
|
|
c1d1e437cc | ||
|
|
2fe0ceb4c7 | ||
|
|
4cba292b57 | ||
|
|
9e91197c5d | ||
|
|
10a8cf3758 | ||
|
|
d1deb35711 | ||
|
|
c4d2b0bff7 | ||
|
|
2d8ceb3255 | ||
|
|
176e5f115b | ||
|
|
9939793e91 | ||
|
|
7d3cd16785 | ||
|
|
7c5fac306a | ||
|
|
37683781d0 | ||
|
|
89dda69205 | ||
|
|
f2c72e5ff8 | ||
|
|
780ebfe120 | ||
|
|
c7d5b687f3 | ||
|
|
5fcb51f372 | ||
|
|
9b08f1b286 | ||
|
|
4f35be7a25 | ||
|
|
884dbff4b8 | ||
|
|
51768eaef9 | ||
|
|
45f579caf2 | ||
|
|
a29dbd88ac | ||
|
|
957337b091 | ||
|
|
4983073172 | ||
|
|
b99d21df69 | ||
|
|
2cfffe6526 | ||
|
|
87a413ea42 | ||
|
|
4146437380 | ||
|
|
b4a7369642 | ||
|
|
f9b51a8abb | ||
|
|
d69d70cfb1 | ||
|
|
ba2d908a89 | ||
|
|
c05abcbccd | ||
|
|
e16fd61bec | ||
|
|
a29d69d8f7 | ||
|
|
e063ad7dda | ||
|
|
7c2bacf3b5 | ||
|
|
c921ca4e65 | ||
|
|
29a36057ed | ||
|
|
5eeecf9214 | ||
|
|
5992abcb7d | ||
|
|
0db7ec3169 | ||
|
|
8046bf98b7 | ||
|
|
9ed39ab0fa | ||
|
|
7e79fc8b5e | ||
|
|
9da68645da | ||
|
|
f7a4b66da1 | ||
|
|
c9212a483b | ||
|
|
cc4e946d95 | ||
|
|
9d1cfd1eb6 | ||
|
|
38969747f4 | ||
|
|
6e7af4c64b | ||
|
|
fb45f9f08c | ||
|
|
6848ce24eb | ||
|
|
dac4fd8d3c | ||
|
|
6905d3e801 | ||
|
|
909b16be64 | ||
|
|
a18162cc47 | ||
|
|
6f0fc9ed49 | ||
|
|
2409c513d6 | ||
|
|
0a95f90012 | ||
|
|
edbd24e942 | ||
|
|
3940af868b | ||
|
|
8b4197d868 | ||
|
|
632e441c24 | ||
|
|
c73ede81ae | ||
|
|
c4b7aeaaa2 | ||
|
|
b5bd98336a | ||
|
|
5af52f6087 | ||
|
|
c5e4d06921 | ||
|
|
917cc00091 | ||
|
|
63cb88bfb8 | ||
|
|
ac1fe15b6c | ||
|
|
ddaa0570bc | ||
|
|
07352743f2 | ||
|
|
f99ef5fff2 | ||
|
|
9d686072e2 | ||
|
|
4e44a2809b | ||
|
|
370e4eafc2 | ||
|
|
b7ec372ebc | ||
|
|
60cdfe4029 | ||
|
|
74e14285ee | ||
|
|
8f56ab54a4 | ||
|
|
4ac58654a0 | ||
|
|
167eb06aeb | ||
|
|
9a0cc7e8c1 | ||
|
|
d4ff1808d5 | ||
|
|
0ff22786cb | ||
|
|
abfb53872c | ||
|
|
67f60a9e09 | ||
|
|
1d04d40507 | ||
|
|
14fdd7cfca | ||
|
|
402ed61756 | ||
|
|
66c75cbb1b | ||
|
|
c32791c7dd | ||
|
|
d6846d8415 | ||
|
|
b1c8efa33f | ||
|
|
f14d031de4 | ||
|
|
25c86db6f5 | ||
|
|
7205d0689e | ||
|
|
cde46012cb | ||
|
|
e4a0122938 | ||
|
|
77c08cb710 | ||
|
|
af49a02047 | ||
|
|
11495c80e3 | ||
|
|
00ab996640 | ||
|
|
a4f77b3e4a | ||
|
|
1839dcdb74 | ||
|
|
6bba37288e | ||
|
|
0c3a12b4d3 | ||
|
|
7e0b590e10 | ||
|
|
009f100375 | ||
|
|
4fdbe3912a | ||
|
|
d4af9130e0 | ||
|
|
d56e2de409 | ||
|
|
6a22cb3021 | ||
|
|
814e8fc73b | ||
|
|
6aedfbd42e | ||
|
|
4207b2c0fb | ||
|
|
f35eb2a2f4 | ||
|
|
4b49782fac | ||
|
|
8fb38d8838 | ||
|
|
925077e30f | ||
|
|
9e07a40ae9 | ||
|
|
c7c3aa2c95 | ||
|
|
b55c70817a | ||
|
|
18934810f1 | ||
|
|
865e4c14a2 | ||
|
|
8cb9f2d742 | ||
|
|
d0d3e5ffe4 | ||
|
|
857f56c286 | ||
|
|
167eb85aa3 | ||
|
|
e2f983542e | ||
|
|
cf622392c0 | ||
|
|
913a83b43d | ||
|
|
b79a3a9c2f | ||
|
|
b3a1ee3127 | ||
|
|
18db00f310 | ||
|
|
9e4d6a9e97 | ||
|
|
21148c6772 | ||
|
|
70a3defc2c | ||
|
|
535399b2e1 | ||
|
|
278111b15b | ||
|
|
a4171ef819 | ||
|
|
7f5518dbf6 | ||
|
|
e102a590ab | ||
|
|
383dc5ab9a | ||
|
|
4dc65f3858 | ||
|
|
e4dccf87d4 | ||
|
|
7dece3732c | ||
|
|
015c22662f | ||
|
|
36b3968667 | ||
|
|
4b9932420b | ||
|
|
87cfd8f538 | ||
|
|
0fc7d78281 | ||
|
|
39086e81ac | ||
|
|
c1233ed692 | ||
|
|
1a401ec1e9 | ||
|
|
79beeebfa6 | ||
|
|
ccf5bea367 | ||
|
|
c77790821d | ||
|
|
95ea2849c2 | ||
|
|
e45c162b3d | ||
|
|
9cdb1a9258 | ||
|
|
241169873b | ||
|
|
690edf1c68 | ||
|
|
f606747dc9 | ||
|
|
f2593d9b4b | ||
|
|
cddd720a15 | ||
|
|
3c25dfe861 | ||
|
|
97a7fc89e0 | ||
|
|
1742bb440c | ||
|
|
73717eaacf | ||
|
|
b84aa7f42e | ||
|
|
ef0f2dae77 | ||
|
|
dcec0b03d4 | ||
|
|
db93981bac | ||
|
|
865bd126f3 | ||
|
|
948952875c | ||
|
|
489ad87ad6 | ||
|
|
62f7bd4fa5 | ||
|
|
353c9b4147 | ||
|
|
50a557b247 | ||
|
|
1dd3ed6057 | ||
|
|
b1c6508a5c | ||
|
|
7bc2e3ebb4 | ||
|
|
cd6b21120c | ||
|
|
51d37f7474 | ||
|
|
61d5e66ad4 | ||
|
|
603547f783 | ||
|
|
bf43be9312 | ||
|
|
06a2dbd281 | ||
|
|
0cedb8d4db | ||
|
|
faa6c97536 | ||
|
|
b65424ee3d | ||
|
|
cf060f353d | ||
|
|
cdb5649709 | ||
|
|
8226e6c6d5 | ||
|
|
0d453f3454 | ||
|
|
80e0978054 | ||
|
|
5a8c567d02 | ||
|
|
9199d24df2 | ||
|
|
41a05adb44 | ||
|
|
95baca6920 | ||
|
|
cff882edc0 | ||
|
|
b9feceba49 | ||
|
|
2d584d115d | ||
|
|
b02fb7ffa8 | ||
|
|
e66506fa5b | ||
|
|
65b4741e27 | ||
|
|
b5e5796549 | ||
|
|
b6945687a6 | ||
|
|
b586a52813 | ||
|
|
000407bcaf | ||
|
|
47989fd139 | ||
|
|
4ce51b81ed | ||
|
|
f63fbaca4d | ||
|
|
7bab5b16a2 | ||
|
|
bdb0a176da | ||
|
|
345a05e4ae | ||
|
|
3bb590c1ae | ||
|
|
9a5e07e078 | ||
|
|
a563881659 | ||
|
|
50ebda332a | ||
|
|
9ae25caf5c | ||
|
|
a27a5c65aa | ||
|
|
395dbedbed | ||
|
|
441f32349f | ||
|
|
fd3311ed60 | ||
|
|
6cd8fb5b58 | ||
|
|
62f1e8d64b | ||
|
|
4eca90e287 | ||
|
|
e6c45e40a9 | ||
|
|
0bb41cc44e | ||
|
|
6c7f5f4888 | ||
|
|
c13d873f1f | ||
|
|
bddeb35520 | ||
|
|
9ce519bb0b | ||
|
|
1f9b0b842f | ||
|
|
69b482849a | ||
|
|
e57fa635ed | ||
|
|
40574a00f7 | ||
|
|
c65bb13ba4 | ||
|
|
713b13b027 | ||
|
|
8cf539a573 | ||
|
|
8a8171bd24 | ||
|
|
d7922571e7 | ||
|
|
f244c11f5e | ||
|
|
436ed26185 | ||
|
|
696daae641 | ||
|
|
5e860c30f3 | ||
|
|
e5b7afe85a | ||
|
|
53a0d63dce | ||
|
|
93a202e8c8 | ||
|
|
66a60f402e | ||
|
|
94900cd386 | ||
|
|
c90cb1f19c | ||
|
|
b06e98ace4 | ||
|
|
59edb355f8 | ||
|
|
a3bff0a697 | ||
|
|
adfe2764e3 | ||
|
|
0d407ce36f | ||
|
|
f3a77d8154 | ||
|
|
932a2b16ac | ||
|
|
8502387af8 | ||
|
|
157484b42a | ||
|
|
839585a3a9 | ||
|
|
9101b5b69d | ||
|
|
f6fa9b4b16 | ||
|
|
826f1fcfa8 | ||
|
|
ede8d5bc60 | ||
|
|
ffde690d9e | ||
|
|
319b95c3fb | ||
|
|
76cf9c4c54 | ||
|
|
c40e5cca80 | ||
|
|
4f7c7800b0 | ||
|
|
f0922c42d1 | ||
|
|
aa56675594 | ||
|
|
19045a86b4 | ||
|
|
15d8613a0e | ||
|
|
19de9bf22f | ||
|
|
84ae93be26 | ||
|
|
d628acc62a | ||
|
|
4cc249e20e | ||
|
|
0d1ebf4e58 | ||
|
|
748ea38e15 | ||
|
|
50ab762905 | ||
|
|
b72d6478d2 | ||
|
|
87cea200a9 | ||
|
|
32ab7c3d4f | ||
|
|
8c63659050 | ||
|
|
23e5af13ad | ||
|
|
09eb14fe37 | ||
|
|
129e831e06 | ||
|
|
1ffe87ee18 | ||
|
|
b1b4177947 | ||
|
|
fe28a8f539 | ||
|
|
86be7b7934 | ||
|
|
5ded99c74a | ||
|
|
a52cee2c45 | ||
|
|
c2cb968b82 | ||
|
|
52fafa115c | ||
|
|
39f7bfe16f | ||
|
|
3c0ba3c8e8 | ||
|
|
db1c480905 | ||
|
|
96b57f9a50 | ||
|
|
0faf245290 | ||
|
|
cee72b5a6d | ||
|
|
76e8cc42c2 | ||
|
|
d22feada57 | ||
|
|
c792621bcb | ||
|
|
d9a58cf27f | ||
|
|
79ba2185fd | ||
|
|
fcf4750d5f | ||
|
|
5c56139b56 | ||
|
|
1f8da968ba | ||
|
|
6ee034784d | ||
|
|
1ab701c100 | ||
|
|
f0661fb11c | ||
|
|
443283de66 | ||
|
|
a4a9c07506 | ||
|
|
866cd1f0e5 | ||
|
|
f539bf9b13 | ||
|
|
c016bcdb1c | ||
|
|
f2fcf8fb11 | ||
|
|
243a94723e | ||
|
|
2437692e69 | ||
|
|
af27154d8d | ||
|
|
7f0604ff8b | ||
|
|
20e281d0a4 | ||
|
|
b8761b3b37 | ||
|
|
ca860f73c2 | ||
|
|
8326f762ab | ||
|
|
0d8a4c1f4d | ||
|
|
bfc9773142 | ||
|
|
f141a27cc2 | ||
|
|
d1c9aa47f5 | ||
|
|
6fbca2e55f | ||
|
|
a3eb59de36 | ||
|
|
e87548becd | ||
|
|
82a79d9a4a | ||
|
|
0200c3b243 | ||
|
|
5a6b7d783b | ||
|
|
c6c8e00f43 | ||
|
|
127eb08f19 | ||
|
|
e6e5c8f733 | ||
|
|
e62a5e18a2 | ||
|
|
550cb28a0e | ||
|
|
ec2da30c74 | ||
|
|
f3583488ef | ||
|
|
57ee2280aa | ||
|
|
75c069111e | ||
|
|
54a4631e22 | ||
|
|
8eaa8999e5 | ||
|
|
979e02ec73 | ||
|
|
dfebcf5294 | ||
|
|
90891504fc | ||
|
|
b3383a24e8 | ||
|
|
bd299f9afb | ||
|
|
9b7088f7fc | ||
|
|
635344a32f | ||
|
|
fc1d3f7fb1 | ||
|
|
334c1c4b5e | ||
|
|
86085d9368 | ||
|
|
f3a84c1d6e | ||
|
|
2a9eb2772a | ||
|
|
9a08f7fec5 | ||
|
|
193842b43b | ||
|
|
cd321f3e0a | ||
|
|
00ad6f7d53 | ||
|
|
88770ed7b6 | ||
|
|
2ec50f6184 | ||
|
|
d45bc0f37b | ||
|
|
03a7a4e210 | ||
|
|
8892fad228 | ||
|
|
b206509345 | ||
|
|
fdee69cd69 | ||
|
|
0d5f3697a1 | ||
|
|
50d4ed827d | ||
|
|
a0218093f2 | ||
|
|
ea920fb67e | ||
|
|
73f166c54a | ||
|
|
5b5cd72f80 | ||
|
|
87b3f91ad3 | ||
|
|
7cefd69b4e | ||
|
|
597089a89b | ||
|
|
b2c76a9e36 | ||
|
|
846be07a5e | ||
|
|
2d3cd8f3dc | ||
|
|
0dfef2699f | ||
|
|
97e74e4afb | ||
|
|
fe04d97d51 | ||
|
|
911917c9d1 | ||
|
|
14bb17435b | ||
|
|
a8c78674bd | ||
|
|
a49de96416 | ||
|
|
9f7dca8288 | ||
|
|
be9c40939e | ||
|
|
eec092ef8d | ||
|
|
8c35b1c1a7 | ||
|
|
61ad81277e | ||
|
|
d98cb6402c | ||
|
|
772a4ce494 | ||
|
|
825673b0c5 | ||
|
|
ea6c698b3a | ||
|
|
d2d6a30623 | ||
|
|
68097291ca | ||
|
|
a8286f77d8 | ||
|
|
d8e96c16bb | ||
|
|
e20c2c56f0 | ||
|
|
823de60e8c | ||
|
|
25fb5fb741 | ||
|
|
017638cc29 | ||
|
|
4e37acf8d4 | ||
|
|
40d273e145 | ||
|
|
88f4ee0f95 | ||
|
|
925b8334a9 | ||
|
|
2e0be8c801 | ||
|
|
6306b8e97d | ||
|
|
927745ca13 | ||
|
|
251b2c4b5c | ||
|
|
c1fd8a5b7b | ||
|
|
7d9b002ef5 | ||
|
|
a03e2387b0 | ||
|
|
3b12ab8b82 | ||
|
|
63a6a8cfd3 | ||
|
|
6ee649f91e | ||
|
|
72646d00e7 | ||
|
|
fe24683495 | ||
|
|
da425533cf | ||
|
|
2024fc8792 | ||
|
|
335e96d1c9 | ||
|
|
936bc882f0 | ||
|
|
7a749e2c56 | ||
|
|
7ed01f55f6 | ||
|
|
76fd1be397 | ||
|
|
5ab1116aed | ||
|
|
1437dde1e1 | ||
|
|
64aca08d34 | ||
|
|
3790d04ed2 | ||
|
|
d1644e62f0 | ||
|
|
96a656bc8a | ||
|
|
763c003487 | ||
|
|
81c251208c | ||
|
|
9ca2c8894d | ||
|
|
d4825d00fb | ||
|
|
591f5a75ef | ||
|
|
15407732ea | ||
|
|
82534f49da | ||
|
|
6c7f76fe96 | ||
|
|
08590f9d98 | ||
|
|
074252a9c0 | ||
|
|
615f7ed2cf | ||
|
|
e3a4435356 | ||
|
|
e8ec2a8d1f | ||
|
|
17cac62c31 | ||
|
|
73beabedea | ||
|
|
93f0e818e4 | ||
|
|
54ff5967fc | ||
|
|
41b18b9419 | ||
|
|
750a2511d5 | ||
|
|
e1c6103dc4 | ||
|
|
5e88a3cfc3 | ||
|
|
f9c71743d1 | ||
|
|
ed4bc87198 | ||
|
|
351e06168e | ||
|
|
75dc134b45 | ||
|
|
53419b9e49 | ||
|
|
aca3e29bd2 | ||
|
|
2fcd6bb3f5 | ||
|
|
25313bf044 | ||
|
|
7012605c9e | ||
|
|
5e8ce33470 | ||
|
|
4dfc037267 | ||
|
|
64ac69a81a | ||
|
|
40297b3d3f | ||
|
|
ead70686b4 | ||
|
|
d0051fbd43 | ||
|
|
0672a32052 | ||
|
|
2371373415 | ||
|
|
0d9101e592 | ||
|
|
0d76b3ac8d | ||
|
|
f9899b36db | ||
|
|
69a9cf9c4a | ||
|
|
60bf7571f3 | ||
|
|
b682816447 | ||
|
|
b2e0ca554f | ||
|
|
97a9ed61a9 | ||
|
|
97fe10b399 | ||
|
|
3c37d6373b | ||
|
|
d4124e95d2 | ||
|
|
e34da34872 | ||
|
|
6686df36a3 | ||
|
|
8ccca887db | ||
|
|
94d5db767b | ||
|
|
8b9e88aa93 | ||
|
|
5afac69500 | ||
|
|
6acce86f73 | ||
|
|
4776921092 | ||
|
|
80f7d12800 | ||
|
|
404e537ae6 | ||
|
|
25d5634a58 | ||
|
|
79b88bbaed | ||
|
|
89695122bb | ||
|
|
58461cc510 | ||
|
|
7f8ec0814e | ||
|
|
47daedec00 | ||
|
|
0ea1a830ac | ||
|
|
b52eef3972 | ||
|
|
323954a293 | ||
|
|
e591e3cb3d | ||
|
|
8abeba8dd1 | ||
|
|
3e2bb6dbe4 | ||
|
|
ce21b4f969 | ||
|
|
fdf6c2c0d9 | ||
|
|
4b68ee5627 | ||
|
|
b899975e36 | ||
|
|
42b747166e | ||
|
|
440c23c4ab | ||
|
|
b399d0ab51 | ||
|
|
59e78cabbf | ||
|
|
25d752aa7f | ||
|
|
dd18fa9e8e | ||
|
|
a9581562cc | ||
|
|
2f48721a7b | ||
|
|
84c54d54f2 | ||
|
|
bf83bd58dc | ||
|
|
aa17fa230f | ||
|
|
f3ecfc32db | ||
|
|
b9aae4b851 | ||
|
|
57a19280dd | ||
|
|
2fa64c6fd4 | ||
|
|
4b70cf67b1 | ||
|
|
e0fee19456 | ||
|
|
abccf1e317 | ||
|
|
587f24738d | ||
|
|
51bcfca3f3 | ||
|
|
7e701c2459 | ||
|
|
89a6792999 | ||
|
|
2cf4bcf71c | ||
|
|
0a31761d74 | ||
|
|
f96b7a5202 | ||
|
|
e381021bdd | ||
|
|
0c1555c76e | ||
|
|
661a03942c | ||
|
|
eb1bd89b19 | ||
|
|
ab788e5792 | ||
|
|
c9f04f8366 | ||
|
|
b50b21db08 | ||
|
|
702d85cde9 | ||
|
|
dd6ed1d623 | ||
|
|
440fd49f65 | ||
|
|
f448a67705 | ||
|
|
7278bd755b | ||
|
|
e69ed2d0ae | ||
|
|
d8175ab867 | ||
|
|
fa08ed0292 | ||
|
|
a8221092e1 | ||
|
|
8d449941d9 | ||
|
|
1f9159d81b | ||
|
|
aba8a5b813 | ||
|
|
a12214ae3a | ||
|
|
e2279e1c79 | ||
|
|
16d17fe78b | ||
|
|
81af4bf1a5 | ||
|
|
66efc5cce8 | ||
|
|
bc9e4c91dd | ||
|
|
898aeed8f4 | ||
|
|
0412f4465c | ||
|
|
2124161744 | ||
|
|
37230dd657 | ||
|
|
9f515a4b4e | ||
|
|
ff5c649cfc | ||
|
|
dc0caed540 | ||
|
|
b33ac1910e | ||
|
|
1aadfe3535 | ||
|
|
ed0ae0140a | ||
|
|
de6ca763a1 | ||
|
|
6c06d72bf1 | ||
|
|
a2413db65d | ||
|
|
2a8faf1d12 | ||
|
|
edff7b8717 | ||
|
|
657cdd07ab | ||
|
|
e1db207487 | ||
|
|
86d47fcdd1 | ||
|
|
e7a71a1cfd | ||
|
|
ce8c50de53 | ||
|
|
3cc0955523 | ||
|
|
3fc8e12d9a | ||
|
|
6671d01c19 | ||
|
|
31cbcc2528 | ||
|
|
00000e14e0 | ||
|
|
f97012b412 | ||
|
|
5fba84ebf5 | ||
|
|
dedc029c91 | ||
|
|
a47271d257 | ||
|
|
5d217dc384 | ||
|
|
0144842be9 | ||
|
|
c0c59ddfbf | ||
|
|
4fb83b7129 | ||
|
|
6512dd6d40 | ||
|
|
f3dd6f9949 | ||
|
|
f992299129 | ||
|
|
b5a8d7e863 | ||
|
|
0d785616dd | ||
|
|
241eb00113 | ||
|
|
9af1565db1 | ||
|
|
129d206946 | ||
|
|
4b14595531 | ||
|
|
b3fa551163 | ||
|
|
5baa2e2902 | ||
|
|
1a916dfba2 | ||
|
|
1eedfd1615 | ||
|
|
ed6fbf67f7 | ||
|
|
704988449f | ||
|
|
a6e52dffe7 | ||
|
|
c39ce8b610 | ||
|
|
3f1521a3c5 | ||
|
|
14f8ed8843 | ||
|
|
e29e967f01 | ||
|
|
ed4c870d0a | ||
|
|
66a684d847 | ||
|
|
c72852832b | ||
|
|
6fee0ac0a9 | ||
|
|
7730cc6170 | ||
|
|
b88aff0a22 | ||
|
|
5add5656fe | ||
|
|
621c7e1682 | ||
|
|
beddab5d03 | ||
|
|
52308cb793 | ||
|
|
4a45f4f877 | ||
|
|
a0e8d50356 | ||
|
|
8d8c6bcee5 | ||
|
|
04521287eb | ||
|
|
8963166bcf | ||
|
|
9f71056b56 | ||
|
|
24acc7e159 | ||
|
|
3825c384bf | ||
|
|
3626d8c642 | ||
|
|
7a5d5d08c0 | ||
|
|
276bb12edb | ||
|
|
28411feff6 | ||
|
|
1118b01dbd | ||
|
|
22221fc413 | ||
|
|
50ece6c1fc | ||
|
|
5de2d60ff8 | ||
|
|
4eed155acb | ||
|
|
5915abd7cb | ||
|
|
52d926d698 | ||
|
|
2b81e983d4 | ||
|
|
55dc7fd988 | ||
|
|
01bcf114b2 | ||
|
|
adbf76a09f | ||
|
|
316081658a | ||
|
|
0aff74afc6 | ||
|
|
f13daafa39 | ||
|
|
ff449b801f | ||
|
|
9bdb72aa06 | ||
|
|
148727917b | ||
|
|
33b25aa981 | ||
|
|
e01c417c1e | ||
|
|
6d050b4d2b | ||
|
|
e9c440ceed | ||
|
|
5cd79ee2b0 | ||
|
|
15c6e22414 | ||
|
|
8d04a0183a | ||
|
|
2f6881934e | ||
|
|
8f7bc59214 | ||
|
|
9bf3b54a83 | ||
|
|
82cd6e320d | ||
|
|
e308b38d6f | ||
|
|
6b7a2e1981 | ||
|
|
d19cb14dc1 | ||
|
|
3e8e454e92 | ||
|
|
f46de92303 | ||
|
|
aeba9542be | ||
|
|
a380044639 | ||
|
|
4cbe50f3a2 | ||
|
|
278d54e780 | ||
|
|
9634598952 | ||
|
|
b60583168b | ||
|
|
ea8630d3d7 | ||
|
|
3cdf578c14 | ||
|
|
f7c0921f18 | ||
|
|
a7ae556478 | ||
|
|
a755bfd22c | ||
|
|
22920a7318 | ||
|
|
cf6a8c333a | ||
|
|
2623bfd2db | ||
|
|
dcc1a93b72 | ||
|
|
3aeea82d2e | ||
|
|
732621f121 | ||
|
|
24e7be4142 | ||
|
|
20c6f0b327 | ||
|
|
e4817518d8 | ||
|
|
35c443f90f | ||
|
|
de669156cd | ||
|
|
d8e3b49b04 | ||
|
|
567f68965d | ||
|
|
65f6892896 | ||
|
|
ec9800b215 | ||
|
|
9010d8f6a1 | ||
|
|
f260945fdf | ||
|
|
b7241825c3 | ||
|
|
c9ed155870 | ||
|
|
69d0a20674 | ||
|
|
b699e8977a | ||
|
|
4f25d8ba89 | ||
|
|
71f5303a5e | ||
|
|
69f91e54e6 | ||
|
|
bcee2c231a | ||
|
|
d1745bb703 | ||
|
|
f468b393c0 | ||
|
|
4b0c38e4ee | ||
|
|
6768bbb486 | ||
|
|
f2c9b46d3e | ||
|
|
b7a3db2ac0 | ||
|
|
307a6654f2 | ||
|
|
f2cc8c77e8 | ||
|
|
830d48255e | ||
|
|
df2b428aa1 | ||
|
|
7d1d05de02 | ||
|
|
921d8b6057 | ||
|
|
0b13ec49f1 | ||
|
|
62ef89f87c | ||
|
|
d0920caf32 | ||
|
|
d0474afdfe | ||
|
|
4ab298dd10 | ||
|
|
db39b89ae4 | ||
|
|
162ae3ead7 | ||
|
|
65a7e8516e | ||
|
|
cccd4af6dd | ||
|
|
751cfdf203 | ||
|
|
898776b617 | ||
|
|
b4db81d6c3 | ||
|
|
e96fdf2a2c | ||
|
|
9052b39f9c | ||
|
|
725725223e | ||
|
|
71b4c3117f | ||
|
|
5e1dc5ac5b | ||
|
|
0b403b7b3a | ||
|
|
be46a00d38 | ||
|
|
22f3412ad0 | ||
|
|
c23a3fcfcd | ||
|
|
1412b0afdb | ||
|
|
60f14b6a68 | ||
|
|
dc440d6cc5 | ||
|
|
7cef7b4d5d | ||
|
|
659a587cdf | ||
|
|
ee72009e73 | ||
|
|
69375f4092 | ||
|
|
76475039b5 | ||
|
|
8a8524a346 | ||
|
|
e7996c693a | ||
|
|
551bd3e284 | ||
|
|
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 |
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,23 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Please only create issues for bug reports. Feature requests or general questions
|
||||
should start as a "Discussion" on GitHub.
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Please only create issues for bug reports. Feature requests or general questions should start as a "Discussion" on GitHub. -->
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
53
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
name: Bug report
|
||||
description: Please only create issues for bug reports. Feature requests or general questions should start as a "Discussion" on GitHub.
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Please make sure to search our issues for similar bugs first! If bug has been reported already, react with a thumbs-up, and/or leave a comment providing further details.
|
||||
- type: textarea
|
||||
id: current
|
||||
attributes:
|
||||
label: Problem and impact
|
||||
description: What problem you're running into? What impact does it have on you / your event?
|
||||
placeholder: When trying to do ____, pretix suddenly shows me an error saying "...".
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected behaviour
|
||||
description: Sometimes bugs are subtle and the expected behaviour may need some explanation. Leave empty if it's just "Don't be broken."
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: "Please give as much context as possible: Are there any settings that impact this behaviour?"
|
||||
placeholder: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
4.
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: If possible, show screenshots of the problem.
|
||||
- type: input
|
||||
id: link
|
||||
attributes:
|
||||
label: Link
|
||||
description: Link to the page where the bug occurs
|
||||
- type: input
|
||||
id: browser
|
||||
attributes:
|
||||
label: Browser (software, desktop or mobile?) and version
|
||||
description: Leave empty for backend problems
|
||||
- type: input
|
||||
id: os
|
||||
attributes:
|
||||
label: Operating system, dependency versions
|
||||
description: Leave empty for frontend problems
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: The pretix version in use. (Leave empty if unknown.)
|
||||
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Community Support
|
||||
url: https://github.com/pretix/pretix/discussions/categories/q-a
|
||||
about: Not sure how to do Y? Please post your support requests in the Q&A section of our GitHub Discussions instead!
|
||||
- name: Feature ideas
|
||||
url: https://github.com/pretix/pretix/discussions/categories/ideas
|
||||
about: Please post your idea in the Ideas section of our GitHub Discussions instead!
|
||||
1
.github/dependabot.yml
vendored
@@ -9,6 +9,7 @@ updates:
|
||||
directory: "/src"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
versioning-strategy: increase
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/src/pretix/static/npm_dir"
|
||||
schedule:
|
||||
|
||||
14
.github/workflows/docs.yml
vendored
@@ -14,16 +14,22 @@ on:
|
||||
- 'src/pretix/static/**'
|
||||
- 'src/tests/**'
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
|
||||
jobs:
|
||||
spelling:
|
||||
name: Spellcheck
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python 3.8
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: 3.9
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
@@ -31,7 +37,7 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install system packages
|
||||
run: sudo apt update && sudo apt install enchant hunspell aspell-en
|
||||
run: sudo apt update && sudo apt install enchant-2 hunspell aspell-en
|
||||
- name: Install Dependencies
|
||||
run: pip3 install -Ur requirements.txt
|
||||
working-directory: ./doc
|
||||
|
||||
20
.github/workflows/strings.yml
vendored
@@ -12,16 +12,22 @@ on:
|
||||
- 'doc/**'
|
||||
- 'src/pretix/locale/**'
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
|
||||
jobs:
|
||||
compile:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
name: Check gettext syntax
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python 3.8
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: 3.9
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
@@ -40,14 +46,14 @@ jobs:
|
||||
run: python manage.py compilejsi18n
|
||||
working-directory: ./src
|
||||
spelling:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
name: Spellcheck
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python 3.8
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: 3.9
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
@@ -55,7 +61,7 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install system packages
|
||||
run: sudo apt update && sudo apt install enchant hunspell hunspell-de-de aspell-en aspell-de
|
||||
run: sudo apt update && sudo apt install enchant-2 hunspell hunspell-de-de aspell-en aspell-de
|
||||
- name: Install Dependencies
|
||||
run: pip3 install -e ".[dev]"
|
||||
working-directory: ./src
|
||||
|
||||
24
.github/workflows/style.yml
vendored
@@ -12,16 +12,22 @@ on:
|
||||
- 'src/pretix/locale/**'
|
||||
- 'src/pretix/static/**'
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
|
||||
jobs:
|
||||
isort:
|
||||
name: isort
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python 3.8
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: 3.9
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
@@ -36,13 +42,13 @@ jobs:
|
||||
working-directory: ./src
|
||||
flake:
|
||||
name: flake8
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python 3.8
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: 3.9
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
@@ -57,13 +63,13 @@ jobs:
|
||||
working-directory: ./src
|
||||
licenseheader:
|
||||
name: licenseheaders
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python 3.8
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: 3.9
|
||||
- name: Install Dependencies
|
||||
run: pip3 install licenseheaders
|
||||
- name: Run licenseheaders
|
||||
|
||||
26
.github/workflows/tests.yml
vendored
@@ -12,23 +12,29 @@ on:
|
||||
- 'doc/**'
|
||||
- 'src/pretix/locale/**'
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
name: Tests
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.6, 3.7, 3.8]
|
||||
python-version: ["3.7", "3.9", "3.10"]
|
||||
database: [sqlite, postgres, mysql]
|
||||
exclude:
|
||||
- database: mysql
|
||||
python-version: 3.7
|
||||
- database: sqlite
|
||||
python-version: 3.7
|
||||
python-version: "3.10"
|
||||
- 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.10"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: getong/mariadb-action@v1.1
|
||||
@@ -55,9 +61,9 @@ 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 -e ".[dev]" mysqlclient psycopg2-binary
|
||||
run: pip3 install --ignore-requires-python -e ".[dev]" mysqlclient psycopg2-binary # We ignore that flake8 needs newer python as we don't run flake8 during tests
|
||||
working-directory: ./src
|
||||
- name: Run checks
|
||||
run: python manage.py check
|
||||
@@ -76,4 +82,4 @@ jobs:
|
||||
with:
|
||||
file: src/coverage.xml
|
||||
fail_ci_if_error: true
|
||||
if: matrix.database == 'postgres' && matrix.python-version == '3.8'
|
||||
if: matrix.database == 'postgres' && matrix.python-version == '3.10'
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
-r doc/requirements.txt
|
||||
15
.readthedocs.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
version: 2
|
||||
sphinx:
|
||||
configuration: doc/conf.py
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3.8"
|
||||
nodejs: "16"
|
||||
apt_packages:
|
||||
- gettext
|
||||
python:
|
||||
install:
|
||||
- method: pip
|
||||
path: ./src/
|
||||
- requirements: doc/requirements.rtd.txt
|
||||
@@ -31,7 +31,7 @@ RUN apt-get update && \
|
||||
echo 'pretixuser ALL=(ALL) NOPASSWD:SETENV: /usr/bin/supervisord' >> /etc/sudoers && \
|
||||
mkdir /static && \
|
||||
mkdir /etc/supervisord && \
|
||||
curl -fsSL https://deb.nodesource.com/setup_15.x | sudo -E bash - && \
|
||||
curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash - && \
|
||||
apt-get install -y nodejs && \
|
||||
curl -qL https://www.npmjs.com/install.sh | sh
|
||||
|
||||
|
||||
20
SECURITY.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Security policy
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
If you discover a vulnerability with our software or server systems, please report it to us in private. Do not to attempt to harm our users, customer's data or our system's availability when looking for vulneratbilities.
|
||||
|
||||
Please contact us at security@pretix.eu with full details and steps to reproduce and allow reasonable time for us to resolve the issue before publishing your findings. If you wish to encrypt your email, you can find our GPG key [here](https://pretix.eu/.well-known/security@pretix.eu.asc).
|
||||
|
||||
We're not large enough to run a formal bug bounty program, but if you find a serious vulnerability in our service, we will find a way to show our gratitude.
|
||||
|
||||
## Version support
|
||||
|
||||
Security support is provided for the current stable release as well as the two previous stable releases.
|
||||
Be sure to keep your pretix installation up to date.
|
||||
|
||||
New releases and security issues will be announced on our [blog](https://pretix.eu/about/en/blog/). If you
|
||||
subscribe to our [newsletter](https://pretix.eu/about/en/blog/) in the "News about self-hosting pretix"
|
||||
category, we will also send you an email on security issues.
|
||||
|
||||
Past security issues are listed [on our website](https://pretix.eu/about/en/security).
|
||||
@@ -2,6 +2,7 @@
|
||||
file=/tmp/supervisor.sock
|
||||
|
||||
[supervisord]
|
||||
environment = AUTOMIGRATE="skip"
|
||||
logfile=/dev/stdout
|
||||
logfile_maxbytes=0
|
||||
loglevel=info
|
||||
|
||||
2
doc/_themes/pretix_theme/layout.html
vendored
@@ -157,7 +157,7 @@
|
||||
<div class="rst-content">
|
||||
{% include "breadcrumbs.html" %}
|
||||
<div role="main" class="document" itemscope="itemscope" itemtype="http://schema.org/Article">
|
||||
<div itemprop="articleBody">
|
||||
<div itemprop="articleBody" class="section">
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
<div class="articleComments">
|
||||
|
||||
@@ -3155,14 +3155,14 @@ a .fa, a .wy-menu-vertical li span.toctree-expand, .wy-menu-vertical li a span.t
|
||||
vertical-align: -15%
|
||||
}
|
||||
|
||||
.wy-alert, .rst-content .note, .rst-content .attention, .rst-content .caution, .rst-content .danger, .rst-content .error, .rst-content .hint, .rst-content .important, .rst-content .tip, .rst-content .warning, .rst-content .seealso, .rst-content .admonition-todo {
|
||||
.wy-alert, .rst-content .note, .rst-content .attention, .rst-content .caution, .rst-content .danger, .rst-content .error, .rst-content .hint, .rst-content .important, .rst-content .tip, .rst-content .warning, .rst-content .seealso, .rst-content .admonition-todo, .rst-content div.deprecated {
|
||||
padding: 12px;
|
||||
line-height: 24px;
|
||||
margin-bottom: 24px;
|
||||
background: #e7f2fa
|
||||
}
|
||||
|
||||
.wy-alert-title, .rst-content .admonition-title {
|
||||
.wy-alert-title, .rst-content .admonition-title, .rst-content .deprecated .versionmodified {
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
@@ -6067,6 +6067,10 @@ url('../opensans_regular_macroman/OpenSans-Regular-webfont.svg#open_sansregular'
|
||||
img.screenshot, a.screenshot img {
|
||||
box-shadow: 0 4px 18px 0 rgba(0,0,0,0.1), 0 6px 20px 0 rgba(0,0,0,0.09);
|
||||
}
|
||||
section > a.screenshot {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
/* Changes */
|
||||
.versionchanged {
|
||||
|
||||
@@ -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``.
|
||||
|
||||
@@ -114,6 +117,9 @@ Example::
|
||||
``loglevel``
|
||||
Set console and file log level (``DEBUG``, ``INFO``, ``WARNING``, ``ERROR`` or ``CRITICAL``). Defaults to ``INFO``.
|
||||
|
||||
``request_id_header``
|
||||
Specifies the name of a header that should be used for logging request IDs. Off by default.
|
||||
|
||||
Locale settings
|
||||
---------------
|
||||
|
||||
@@ -220,12 +226,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 +306,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
|
||||
@@ -375,9 +399,9 @@ 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``
|
||||
To use redis with sentinels set the broker or backend to ``sentinel://sentinel_host_1:26379;sentinel_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 instances behind the sentinel have a password use ``sentinel://:my_password@sentinel_host_1:26379;sentinel_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
|
||||
@@ -445,8 +469,10 @@ You can configure the maximum file size for uploading various files::
|
||||
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 in MiB, defaults to 10 MiB
|
||||
; 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
|
||||
|
||||
@@ -36,9 +36,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**.
|
||||
|
||||
.. 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.
|
||||
@@ -61,6 +58,9 @@ 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. Please make sure that UTF8 is used as encoding for the best compatibility. You can check this with
|
||||
the following command::
|
||||
@@ -91,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
|
||||
-----
|
||||
|
||||
@@ -106,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
|
||||
@@ -226,6 +240,9 @@ The following snippet is an example on how to configure a nginx proxy for pretix
|
||||
listen 80 default_server;
|
||||
listen [::]:80 ipv6only=on default_server;
|
||||
server_name pretix.mydomain.com;
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
server {
|
||||
listen 443 default_server;
|
||||
|
||||
@@ -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,6 +47,9 @@ 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. Please make sure that UTF8 is used as encoding for the
|
||||
best compatibility. You can check this with the following command::
|
||||
@@ -65,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
|
||||
--------------------
|
||||
|
||||
@@ -142,7 +144,7 @@ If you're running MySQL, also install the client library::
|
||||
|
||||
(venv)$ pip3 install mysqlclient
|
||||
|
||||
Note that you need Python 3.6 or newer. You can find out your Python version using ``python -V``.
|
||||
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::
|
||||
|
||||
@@ -223,6 +225,9 @@ The following snippet is an example on how to configure a nginx proxy for pretix
|
||||
listen 80 default_server;
|
||||
listen [::]:80 ipv6only=on default_server;
|
||||
server_name pretix.mydomain.com;
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
server {
|
||||
listen 443 default_server;
|
||||
@@ -259,14 +264,14 @@ The following snippet is an example on how to configure a nginx proxy for pretix
|
||||
}
|
||||
|
||||
location /static/ {
|
||||
alias /var/pretix/venv/lib/python3.7/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.7`` 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.
|
||||
@@ -313,6 +318,27 @@ example::
|
||||
(venv)$ python -m pretix rebuild
|
||||
# systemctl restart pretix-web pretix-worker
|
||||
|
||||
System updates
|
||||
--------------
|
||||
|
||||
After system updates, such as updates to a new Ubuntu or Debian release, you might be using a new Python version.
|
||||
That's great, but requires some adjustments. First, adjust any old version paths in your nginx configuration file.
|
||||
Then, re-create your Python environment::
|
||||
|
||||
$ source /var/pretix/venv/bin/activate
|
||||
(venv)$ pip3 freeze > /tmp/pip-backup.txt
|
||||
$ rm -rf /var/pretix/venv
|
||||
$ python3 -m venv /var/pretix/venv
|
||||
$ source /var/pretix/venv/bin/activate
|
||||
(venv)$ pip3 install -U pip wheel setuptools
|
||||
(venv)$ pip3 install -r /tmp/pip-backup.txt
|
||||
|
||||
Then, proceed like after any plugin installation::
|
||||
|
||||
(venv)$ python -m pretix migrate
|
||||
(venv)$ python -m pretix rebuild
|
||||
(venv)$ python -m pretix updatestyles
|
||||
# systemctl restart pretix-web pretix-worker
|
||||
|
||||
.. _Postfix: https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-postfix-as-a-send-only-smtp-server-on-ubuntu-16-04
|
||||
.. _nginx: https://botleg.com/stories/https-with-lets-encrypt-and-nginx/
|
||||
|
||||
@@ -99,11 +99,43 @@ 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.
|
||||
|
||||
Device Information
|
||||
------------------
|
||||
|
||||
You can request information about your device and the server with one call:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/device/info HTTP/1.1
|
||||
Host: pretix.eu
|
||||
|
||||
The response will look like this:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"device": {
|
||||
"organizer": "foo",
|
||||
"device_id": 5,
|
||||
"unique_serial": "HHZ9LW9JWP390VFZ",
|
||||
"api_token": "1kcsh572fonm3hawalrncam4l1gktr2rzx25a22l8g9hx108o9oi0rztpcvwnfnd",
|
||||
"name": "Bar",
|
||||
"gate": {
|
||||
"id": 3,
|
||||
"name": "South entrance"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Creating a new API key
|
||||
----------------------
|
||||
|
||||
|
||||
@@ -43,13 +43,16 @@ Possible permissions are:
|
||||
* Can view vouchers
|
||||
* Can change vouchers
|
||||
|
||||
.. _`rest-compat`:
|
||||
|
||||
Compatibility
|
||||
-------------
|
||||
|
||||
We currently see pretix' API as a beta-stage feature. We therefore do not give any guarantees
|
||||
for compatibility between feature releases of pretix (such as 1.5 and 1.6). However, as always,
|
||||
we try not to break things when we don't need to. Any backwards-incompatible changes will be
|
||||
prominently noted in the release notes.
|
||||
We try to avoid any breaking changes to our API to avoid hassle on your end. If possible, we'll
|
||||
build new features in a way that keeps all pre-existing API usage unchanged. In some cases,
|
||||
this might not be possible or only possible with restrictions. In these case, any
|
||||
backwards-incompatible changes will be prominently noted in the "Changes to the REST API"
|
||||
section of our release notes. If possible, we will announce them multiple releases in advance.
|
||||
|
||||
We treat the following types of changes as *backwards-compatible* so we ask you to make sure
|
||||
that your clients can deal with them properly:
|
||||
@@ -58,6 +61,8 @@ that your clients can deal with them properly:
|
||||
* Support of new HTTP methods for a given API endpoint
|
||||
* Support of new query parameters for a given API endpoint
|
||||
* New fields contained in API responses
|
||||
* New possible values of enumeration-like fields
|
||||
* Response body structure or message texts on failed requests (``4xx``, ``5xx`` response codes)
|
||||
|
||||
We treat the following types of changes as *backwards-incompatible*:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -17,8 +17,8 @@ The cart position resource contains the following public fields:
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal ID of the cart position
|
||||
cart_id string Identifier of the cart this belongs to. Needs to end
|
||||
in "@api" for API-created positions.
|
||||
cart_id string Identifier of the cart this belongs to, needs to end
|
||||
in "@api" for API-created positions
|
||||
datetime datetime Time of creation
|
||||
expires datetime The cart position will expire at this time and no longer block quota
|
||||
item integer ID of the item
|
||||
@@ -29,22 +29,23 @@ attendee_name_parts object of strings Composition of
|
||||
attendee_email string Specified attendee email address for this position (or ``null``)
|
||||
voucher integer Internal ID of the voucher used for this position (or ``null``)
|
||||
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``).
|
||||
is_bundled boolean If ``addon_to`` is set, this shows whether this is a bundled product or an addon product
|
||||
subevent integer ID of the date inside an event series this position belongs to (or ``null``)
|
||||
answers list of objects Answers to user-defined questions
|
||||
├ question integer Internal ID of the answered question
|
||||
├ answer string Text representation of the answer
|
||||
├ question_identifier string The question's ``identifier`` field
|
||||
├ options list of integers Internal IDs of selected option(s)s (only for choice types)
|
||||
└ option_identifiers list of strings The ``identifier`` fields of the selected option(s)s
|
||||
seat objects The assigned seat. Can be ``null``.
|
||||
seat objects The assigned seat (or ``null``)
|
||||
├ id integer Internal ID of the seat instance
|
||||
├ name string Human-readable seat name
|
||||
└ seat_guid string Identifier of the seat within the seating plan
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
.. versionchanged:: 4.14
|
||||
|
||||
This ``seat`` attribute has been added.
|
||||
This ``is_bundled`` attribute has been added and the cart creation endpoints have been updated.
|
||||
|
||||
|
||||
Cart position endpoints
|
||||
@@ -87,6 +88,7 @@ Cart position endpoints
|
||||
"attendee_email": null,
|
||||
"voucher": null,
|
||||
"addon_to": null,
|
||||
"is_bundled": false,
|
||||
"subevent": null,
|
||||
"datetime": "2018-06-11T10:00:00Z",
|
||||
"expires": "2018-06-11T10:00:00Z",
|
||||
@@ -133,6 +135,7 @@ Cart position endpoints
|
||||
"attendee_email": null,
|
||||
"voucher": null,
|
||||
"addon_to": null,
|
||||
"is_bundled": false,
|
||||
"subevent": null,
|
||||
"datetime": "2018-06-11T10:00:00Z",
|
||||
"expires": "2018-06-11T10:00:00Z",
|
||||
@@ -168,16 +171,16 @@ Cart position endpoints
|
||||
|
||||
* does not validate if the event's ticket sales are already over or haven't started
|
||||
|
||||
* does not support add-on products at the moment
|
||||
* does not validate constraints on add-on products at the moment
|
||||
|
||||
* does not check or calculate prices but believes any prices you send
|
||||
|
||||
* does not support the redemption of vouchers
|
||||
|
||||
* does not prevent you from buying items that can only be bought with a voucher
|
||||
|
||||
* does not support file upload questions
|
||||
|
||||
Note that more validation might be added in the future, so please do not rely on missing validation.
|
||||
|
||||
You can supply the following fields of the resource:
|
||||
|
||||
* ``cart_id`` (optional, needs to end in ``@api``)
|
||||
@@ -189,8 +192,11 @@ Cart position endpoints
|
||||
* ``attendee_email`` (optional)
|
||||
* ``subevent`` (optional)
|
||||
* ``expires`` (optional)
|
||||
* ``includes_tax`` (optional)
|
||||
* ``includes_tax`` (optional, **deprecated**, do not use, will be removed)
|
||||
* ``sales_channel`` (optional)
|
||||
* ``voucher`` (optional, expect a voucher code)
|
||||
* ``addons`` (optional, expect a list of nested objects of cart positions)
|
||||
* ``bundled`` (optional, expect a list of nested objects of cart positions)
|
||||
* ``answers``
|
||||
|
||||
* ``question``
|
||||
@@ -222,6 +228,12 @@ Cart position endpoints
|
||||
"options": []
|
||||
}
|
||||
],
|
||||
"addons": [
|
||||
{
|
||||
"item": 2,
|
||||
"variation": null,
|
||||
}
|
||||
],
|
||||
"subevent": null
|
||||
}
|
||||
|
||||
@@ -233,7 +245,7 @@ Cart position endpoints
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
(Full cart position resource, see above.)
|
||||
(Full cart position resource, see above, with additional nested objects "addons" and "bundled".)
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer of the event to create a position for
|
||||
:param event: The ``slug`` field of the event to create a position for
|
||||
@@ -245,8 +257,8 @@ Cart position endpoints
|
||||
|
||||
.. 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!
|
||||
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.
|
||||
|
||||
|
||||
346
doc/api/resources/checkin.rst
Normal file
@@ -0,0 +1,346 @@
|
||||
.. spelling:: checkin
|
||||
|
||||
.. _rest-checkin:
|
||||
|
||||
Check-in
|
||||
========
|
||||
|
||||
This page describes special APIs built for ticket scanning apps. For managing check-in configuration or other operations,
|
||||
please also see :ref:`rest-checkinlists`. The check-in list API also contains endpoints to obtain statistics or log
|
||||
failed scans.
|
||||
|
||||
.. versionchanged:: 4.12
|
||||
|
||||
The endpoints listed on this page have been added.
|
||||
|
||||
.. _`rest-checkin-redeem`:
|
||||
|
||||
Checking a ticket in
|
||||
--------------------
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/checkinrpc/redeem/
|
||||
|
||||
Tries to redeem an order position, i.e. checks the attendee in (or out). This is the recommended endpoint to use
|
||||
if you build any kind of scanning app that performs check-ins for scanned barcodes. It is safe to use with untrusted
|
||||
inputs in the ``secret`` field.
|
||||
|
||||
This endpoint supports passing multiple check-in lists to perform a multi-event scan. However, each check-in list
|
||||
passed needs to be from a distinct event.
|
||||
|
||||
:<json string secret: Scanned QR code corresponding to the ``secret`` attribute of a ticket.
|
||||
:<json array lists: List of check-in list IDs to search on. No two check-in lists may be from the same event.
|
||||
:<json string type: Send ``"exit"`` for an exit and ``"entry"`` (default) for an entry.
|
||||
:<json datetime datetime: Specifies the datetime of the check-in. If not supplied, the current time will be used.
|
||||
:<json boolean force: Specifies that the check-in should succeed regardless of revoked barcode, previous check-ins or required
|
||||
questions that have not been filled. This is usually used to upload offline scans that already happened,
|
||||
because there's no point in validating them since they happened whether they are valid or not. Defaults to ``false``.
|
||||
:<json boolean questions_supported: When this parameter is set to ``true``, handling of questions is supported. If
|
||||
you do not implement question handling in your user interface, you **must**
|
||||
set this to ``false``. In that case, questions will just be ignored. Defaults
|
||||
to ``true``.
|
||||
:<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
|
||||
list.
|
||||
:<json object answers: If questions are supported/required, you may/must supply a mapping of question IDs to their
|
||||
respective answers. The answers should always be strings. In case of (multiple-)choice-type
|
||||
answers, the string should contain the (comma-separated) IDs of the selected options.
|
||||
:<json string nonce: You can set this parameter to a unique random value to identify this check-in. If you're sending
|
||||
this request twice with the same nonce, the second request will also succeed but will always
|
||||
create only one check-in object even when the previous request was successful as well. This
|
||||
allows for a certain level of idempotency and enables you to re-try after a connection failure.
|
||||
:>json string status: ``"ok"``, ``"incomplete"``, or ``"error"``
|
||||
:>json string reason: Reason code, only set on status ``"error"``, see below for possible values.
|
||||
:>json string reason_explanation: Human-readable explanation, only set on status ``"error"`` and reason ``"rules"``, can be null.
|
||||
:>json object position: Copy of the matching order position (if any was found). The contents are the same as the
|
||||
:ref:`order-position-resource`, with the following differences: (1) The ``checkins`` value
|
||||
will only include check-ins for the selected list. (2) An additional boolean property
|
||||
``require_attention`` will inform you whether either the order or the item have the
|
||||
``checkin_attention`` flag set. (3) If ``attendee_name`` is empty, it may automatically fall
|
||||
back to values from a parent product or from invoice addresses.
|
||||
:>json boolean require_attention: Whether or not the ``require_attention`` flag is set on the item or order.
|
||||
:>json object list: Excerpt of information about the matching :ref:`check-in list <rest-checkinlists>` (if any was found),
|
||||
including the attributes ``id``, ``name``, ``event``, ``subevent``, and ``include_pending``.
|
||||
:>json object questions: List of questions to be answered for check-in, only set on status ``"incomplete"``.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/checkinrpc/redeem/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"secret": "M5BO19XmFwAjLd4nDYUAL9ISjhti0e9q",
|
||||
"lists": [1],
|
||||
"force": false,
|
||||
"ignore_unpaid": false,
|
||||
"nonce": "Pvrk50vUzQd0DhdpNRL4I4OcXsvg70uA",
|
||||
"datetime": null,
|
||||
"questions_supported": true,
|
||||
"answers": {
|
||||
"4": "XS"
|
||||
}
|
||||
}
|
||||
|
||||
**Example successful response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"status": "ok",
|
||||
"position": {
|
||||
…
|
||||
},
|
||||
"require_attention": false,
|
||||
"list": {
|
||||
"id": 1,
|
||||
"name": "Default check-in list",
|
||||
"event": "sampleconf",
|
||||
"subevent": null,
|
||||
"include_pending": false
|
||||
}
|
||||
}
|
||||
|
||||
**Example response with required questions**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 400 Bad Request
|
||||
Content-Type: text/json
|
||||
|
||||
{
|
||||
"status": "incomplete",
|
||||
"position": {
|
||||
…
|
||||
},
|
||||
"require_attention": false,
|
||||
"list": {
|
||||
"id": 1,
|
||||
"name": "Default check-in list",
|
||||
"event": "sampleconf",
|
||||
"subevent": null,
|
||||
"include_pending": false
|
||||
},
|
||||
"questions": [
|
||||
{
|
||||
"id": 1,
|
||||
"question": {"en": "T-Shirt size"},
|
||||
"type": "C",
|
||||
"required": false,
|
||||
"items": [1, 2],
|
||||
"position": 1,
|
||||
"identifier": "WY3TP9SL",
|
||||
"ask_during_checkin": true,
|
||||
"options": [
|
||||
{
|
||||
"id": 1,
|
||||
"identifier": "LVETRWVU",
|
||||
"position": 0,
|
||||
"answer": {"en": "S"}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"identifier": "DFEMJWMJ",
|
||||
"position": 1,
|
||||
"answer": {"en": "M"}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"identifier": "W9AH7RDE",
|
||||
"position": 2,
|
||||
"answer": {"en": "L"}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
**Example error response (invalid ticket)**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 404 Not Found
|
||||
Content-Type: text/json
|
||||
|
||||
{
|
||||
"detail": "Not found.",
|
||||
"status": "error",
|
||||
"reason": "invalid",
|
||||
"reason_explanation": null,
|
||||
"require_attention": false
|
||||
}
|
||||
|
||||
**Example error response (known, but invalid ticket)**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/json
|
||||
|
||||
{
|
||||
"status": "error",
|
||||
"reason": "unpaid",
|
||||
"reason_explanation": null,
|
||||
"require_attention": false,
|
||||
"list": {
|
||||
"id": 1,
|
||||
"name": "Default check-in list",
|
||||
"event": "sampleconf",
|
||||
"subevent": null,
|
||||
"include_pending": false
|
||||
},
|
||||
"position": {
|
||||
…
|
||||
}
|
||||
}
|
||||
|
||||
Possible error reasons:
|
||||
|
||||
* ``invalid`` - Ticket is not known.
|
||||
* ``unpaid`` - Ticket is not paid for.
|
||||
* ``canceled`` – Ticket is canceled or expired.
|
||||
* ``already_redeemed`` - Ticket already has been redeemed.
|
||||
* ``product`` - Tickets with this product may not be scanned at this device.
|
||||
* ``rules`` - Check-in prevented by a user-defined rule.
|
||||
* ``ambiguous`` - Multiple tickets match scan, rejected.
|
||||
* ``revoked`` - Ticket code has been revoked.
|
||||
* ``error`` - Internal error.
|
||||
|
||||
In case of reason ``rules``, there might be an additional response field ``reason_explanation`` with a human-readable
|
||||
description of the violated rules. However, that field can also be missing or be ``null``.
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:statuscode 201: no error
|
||||
:statuscode 400: Invalid or incomplete request, see above
|
||||
: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 does not exist.
|
||||
|
||||
Performing a ticket search
|
||||
--------------------------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/checkinrpc/search/
|
||||
|
||||
Returns a list of all order positions matching a given search request. The result is the same as
|
||||
the :ref:`order-position-resource`, with the following differences:
|
||||
|
||||
* The ``checkins`` value will only include check-ins for the selected list.
|
||||
|
||||
* An additional boolean property ``require_attention`` will inform you whether either the order or the item
|
||||
have the ``checkin_attention`` flag set.
|
||||
|
||||
* If ``attendee_name`` is empty, it will automatically fall back to values from a parent product or from invoice
|
||||
addresses.
|
||||
|
||||
This endpoint supports passing multiple check-in lists to perform a multi-event search. However, each check-in list
|
||||
passed needs to be from a distinct event.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/checkinrpc/search/?list=1&search=Peter 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": 23442,
|
||||
"order": "ABC12",
|
||||
"positionid": 1,
|
||||
"item": 1345,
|
||||
"variation": null,
|
||||
"price": "23.00",
|
||||
"attendee_name": "Peter",
|
||||
"attendee_name_parts": {
|
||||
"full_name": "Peter",
|
||||
},
|
||||
"attendee_email": null,
|
||||
"voucher": null,
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": null,
|
||||
"tax_value": "0.00",
|
||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||
"addon_to": null,
|
||||
"subevent": null,
|
||||
"pseudonymization_id": "MQLJvANO3B",
|
||||
"seat": null,
|
||||
"checkins": [
|
||||
{
|
||||
"list": 1,
|
||||
"type": "entry",
|
||||
"gate": null,
|
||||
"device": 2,
|
||||
"datetime": "2017-12-25T12:45:23Z",
|
||||
"auto_checked_in": true
|
||||
}
|
||||
],
|
||||
"answers": [
|
||||
{
|
||||
"question": 12,
|
||||
"answer": "Foo",
|
||||
"options": []
|
||||
}
|
||||
],
|
||||
"downloads": [
|
||||
{
|
||||
"output": "pdf",
|
||||
"url": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/download/pdf/"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query string search: Fuzzy search matching the attendee name, order code, invoice address name as well as to the beginning of the secret.
|
||||
:query integer list: The check-in list to search on, can be passed multiple times.
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:query string ignore_status: If set to ``true``, results will be returned regardless of the state of
|
||||
the order they belong to and you will need to do your own filtering by order status.
|
||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``order__code``,
|
||||
``order__datetime``, ``positionid``, ``attendee_name``, ``last_checked_in`` and ``order__email``. Default:
|
||||
``attendee_name,positionid``
|
||||
:query string order: Only return positions of the order with the given order code
|
||||
:query string search: Fuzzy search matching the attendee name, order code, invoice address name as well as to the beginning of the secret.
|
||||
:query string expand: Expand a field into a full object. Currently only ``subevent``, ``item``, and ``variation`` are supported. Can be passed multiple times.
|
||||
:query integer item: Only return positions with the purchased item matching the given ID.
|
||||
:query integer item__in: Only return positions with the purchased item matching one of the given comma-separated IDs.
|
||||
:query integer variation: Only return positions with the purchased item variation matching the given ID.
|
||||
:query integer variation__in: Only return positions with one of the purchased item variation matching the given
|
||||
comma-separated IDs.
|
||||
:query string attendee_name: Only return positions with the given value in the attendee_name field. Also, add-on
|
||||
products positions are shown if they refer to an attendee with the given name.
|
||||
:query string secret: Only return positions with the given ticket secret.
|
||||
:query string order__status: Only return positions with the given order status.
|
||||
:query string order__status__in: Only return positions with one the given comma-separated order status.
|
||||
:query boolean has_checkin: If set to ``true`` or ``false``, only return positions that have or have not been
|
||||
checked in already.
|
||||
:query integer subevent: Only return positions of the sub-event with the given ID
|
||||
:query integer subevent__in: Only return positions of one of the sub-events with the given comma-separated IDs
|
||||
:query integer addon_to: Only return positions that are add-ons to the position with the given ID.
|
||||
:query integer addon_to__in: Only return positions that are add-ons to one of the positions with the given
|
||||
comma-separated IDs.
|
||||
:query string voucher: Only return positions with a specific voucher.
|
||||
:query string voucher__code: Only return positions with a specific voucher code.
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer or check-in list does not exist **or** you have no permission to view this resource.
|
||||
:statuscode 404: The requested check-in list does not exist.
|
||||
@@ -1,5 +1,7 @@
|
||||
.. spelling:: checkin
|
||||
|
||||
.. _rest-checkinlists:
|
||||
|
||||
Check-in lists
|
||||
==============
|
||||
|
||||
@@ -34,24 +36,12 @@ allow_multiple_entries boolean If ``true``, su
|
||||
allow_entry_after_exit boolean If ``true``, subsequent scans of a ticket on this list are valid if the last scan of the ticket was an exit scan.
|
||||
rules object Custom check-in logic. The contents of this field are currently not considered a stable API and modifications through the API are highly discouraged.
|
||||
exit_all_at datetime Automatically check out (i.e. perform an exit scan) at this point in time. After this happened, this property will automatically be set exactly one day into the future. Note that this field is considered "internal configuration" and if you pull the list with ``If-Modified-Since``, the daily change in this field will not trigger a response.
|
||||
addon_match boolean If ``true``, tickets on this list can be redeemed by scanning their parent ticket if this still leads to an unambiguous match.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 3.9
|
||||
.. versionchanged:: 4.12
|
||||
|
||||
The ``subevent`` attribute may now be ``null`` inside event series. The ``allow_multiple_entries``,
|
||||
``allow_entry_after_exit``, and ``rules`` attributes have been added.
|
||||
|
||||
.. versionchanged:: 3.11
|
||||
|
||||
The ``subevent_match`` and ``exclude`` query parameters have been added.
|
||||
|
||||
.. versionchanged:: 3.12
|
||||
|
||||
The ``exit_all_at`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 3.17
|
||||
|
||||
The ``ends_after`` and ``expand`` query parameters have been added.
|
||||
The ``addon_match`` attribute has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
@@ -94,6 +84,7 @@ Endpoints
|
||||
"allow_entry_after_exit": true,
|
||||
"exit_all_at": null,
|
||||
"rules": {},
|
||||
"addon_match": false,
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
@@ -146,6 +137,7 @@ Endpoints
|
||||
"allow_entry_after_exit": true,
|
||||
"exit_all_at": null,
|
||||
"rules": {},
|
||||
"addon_match": false,
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
@@ -245,6 +237,7 @@ Endpoints
|
||||
"subevent": null,
|
||||
"allow_multiple_entries": false,
|
||||
"allow_entry_after_exit": true,
|
||||
"addon_match": false,
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
@@ -269,6 +262,7 @@ Endpoints
|
||||
"subevent": null,
|
||||
"allow_multiple_entries": false,
|
||||
"allow_entry_after_exit": true,
|
||||
"addon_match": false,
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
@@ -323,6 +317,7 @@ Endpoints
|
||||
"subevent": null,
|
||||
"allow_multiple_entries": false,
|
||||
"allow_entry_after_exit": true,
|
||||
"addon_match": false,
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
@@ -415,6 +410,9 @@ Order position endpoints
|
||||
* If ``attendee_name`` is empty, it will automatically fall back to values from a parent product or from invoice
|
||||
addresses.
|
||||
|
||||
You can use this endpoint to implement a ticket search. We also provide a dedicated search input as part of our
|
||||
:ref:`check-in API <rest-checkin>` that supports search across multiple events.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
@@ -604,15 +602,23 @@ 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
|
||||
accepts a number of optional requests in the body.
|
||||
|
||||
**Tip:** Instead of an ID, you can also use the ``secret`` field as the lookup parameter.
|
||||
**Tip:** Instead of an ID, you can also use the ``secret`` field as the lookup parameter. In this case, you should
|
||||
always set ``untrusted_input=true`` as a query parameter to avoid security issues.
|
||||
|
||||
.. note::
|
||||
|
||||
We no longer recommend using this API if you're building a ticket scanning application, as it has a few design
|
||||
flaws that can lead to `security issues`_ or compatibility issues due to barcode content characters that are not
|
||||
URL-safe. We recommend to use our new :ref:`check-in API <rest-checkin>` instead.
|
||||
|
||||
:query boolean untrusted_input: If set to true, the lookup parameter is **always** interpreted as a ``secret``, never
|
||||
as an ``id``. This should be always set if you are passing through untrusted, scanned
|
||||
data to avoid guessing of ticket IDs.
|
||||
:<json boolean questions_supported: When this parameter is set to ``true``, handling of questions is supported. If
|
||||
you do not implement question handling in your user interface, you **must**
|
||||
set this to ``false``. In that case, questions will just be ignored. Defaults
|
||||
@@ -733,12 +739,15 @@ Order position endpoints
|
||||
|
||||
Possible error reasons:
|
||||
|
||||
* ``unpaid`` - Ticket is not paid for
|
||||
* ``canceled`` – Ticket is canceled or expired. This reason is only sent when your request sets
|
||||
* ``invalid`` - Ticket code not known.
|
||||
* ``unpaid`` - Ticket is not paid for.
|
||||
* ``canceled`` – Ticket is canceled or expired. This reason is only sent when your request sets.
|
||||
``canceled_supported`` to ``true``, otherwise these orders return ``unpaid``.
|
||||
* ``already_redeemed`` - Ticket already has been redeemed
|
||||
* ``product`` - Tickets with this product may not be scanned at this device
|
||||
* ``rules`` - Check-in prevented by a user-defined rule
|
||||
* ``already_redeemed`` - Ticket already has been redeemed.
|
||||
* ``product`` - Tickets with this product may not be scanned at this device.
|
||||
* ``rules`` - Check-in prevented by a user-defined rule.
|
||||
* ``ambiguous`` - Multiple tickets match scan, rejected.
|
||||
* ``revoked`` - Ticket code has been revoked.
|
||||
|
||||
In case of reason ``rules``, there might be an additional response field ``reason_explanation`` with a human-readable
|
||||
description of the violated rules. However, that field can also be missing or be ``null``.
|
||||
@@ -752,3 +761,6 @@ Order position endpoints
|
||||
: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.
|
||||
|
||||
|
||||
.. _security issues: https://pretix.eu/about/de/blog/20220705-release-4111/
|
||||
@@ -14,6 +14,10 @@ The customer resource contains the following public fields:
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
identifier string Internal ID of the customer
|
||||
external_identifier string External ID of the customer (or ``null``). This field can
|
||||
be changed for customers created manually or through
|
||||
the API, but is read-only for customers created through a
|
||||
SSO integration.
|
||||
email string Customer email address
|
||||
name string Name of this customer (or ``null``)
|
||||
name_parts object of strings Decomposition of name (i.e. given name, family name)
|
||||
@@ -24,10 +28,17 @@ last_login datetime Date and time o
|
||||
date_joined datetime Date and time of registration
|
||||
locale string Preferred language of the customer
|
||||
last_modified datetime Date and time of modification of the record
|
||||
notes string Internal notes and comments (or ``null``)
|
||||
password string Can only be set during creation of a new customer, will
|
||||
not be included in any responses.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionadded:: 4.0
|
||||
|
||||
.. versionchanged:: 4.3
|
||||
|
||||
Passwords can now be set through the API during customer creation.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -58,6 +69,7 @@ Endpoints
|
||||
"results": [
|
||||
{
|
||||
"identifier": "8WSAJCJ",
|
||||
"external_identifier": null,
|
||||
"email": "customer@example.org",
|
||||
"name": "John Doe",
|
||||
"name_parts": {
|
||||
@@ -69,7 +81,8 @@ Endpoints
|
||||
"last_login": null,
|
||||
"date_joined": "2021-04-06T13:44:22.809216Z",
|
||||
"locale": "de",
|
||||
"last_modified": "2021-04-06T13:44:22.809377Z"
|
||||
"last_modified": "2021-04-06T13:44:22.809377Z",
|
||||
"notes": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -103,6 +116,7 @@ Endpoints
|
||||
|
||||
{
|
||||
"identifier": "8WSAJCJ",
|
||||
"external_identifier": null,
|
||||
"email": "customer@example.org",
|
||||
"name": "John Doe",
|
||||
"name_parts": {
|
||||
@@ -114,7 +128,8 @@ Endpoints
|
||||
"last_login": null,
|
||||
"date_joined": "2021-04-06T13:44:22.809216Z",
|
||||
"locale": "de",
|
||||
"last_modified": "2021-04-06T13:44:22.809377Z"
|
||||
"last_modified": "2021-04-06T13:44:22.809377Z",
|
||||
"notes": null
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
@@ -125,7 +140,9 @@ Endpoints
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/customers/
|
||||
|
||||
Creates a new customer
|
||||
Creates a new customer. In addition to the fields defined on the resource, you can pass the field ``send_email``
|
||||
to control whether the system should send an account activation email with a password reset link (defaults to
|
||||
``false``).
|
||||
|
||||
**Example request**:
|
||||
|
||||
@@ -137,7 +154,9 @@ Endpoints
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"email": "test@example.org"
|
||||
"email": "test@example.org",
|
||||
"password": "verysecret",
|
||||
"send_email": true
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@@ -150,6 +169,7 @@ Endpoints
|
||||
|
||||
{
|
||||
"identifier": "8WSAJCJ",
|
||||
"external_identifier": null,
|
||||
"email": "test@example.org",
|
||||
...
|
||||
}
|
||||
@@ -166,8 +186,8 @@ Endpoints
|
||||
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
|
||||
want to change.
|
||||
|
||||
You can change all fields of the resource except the ``identifier``, ``last_login``, ``date_joined``, ``name``,
|
||||
and ``last_modified`` fields.
|
||||
You can change all fields of the resource except the ``identifier``, ``last_login``, ``date_joined``,
|
||||
``name`` (which is auto-generated from ``name_parts``), and ``last_modified`` fields.
|
||||
|
||||
**Example request**:
|
||||
|
||||
@@ -193,6 +213,7 @@ Endpoints
|
||||
|
||||
{
|
||||
"identifier": "8WSAJCJ",
|
||||
"external_identifier": null,
|
||||
"email": "test@example.org",
|
||||
…
|
||||
}
|
||||
@@ -226,6 +247,7 @@ Endpoints
|
||||
|
||||
{
|
||||
"identifier": "8WSAJCJ",
|
||||
"external_identifier": null,
|
||||
"email": null,
|
||||
…
|
||||
}
|
||||
|
||||
306
doc/api/resources/discounts.rst
Normal file
@@ -0,0 +1,306 @@
|
||||
.. _`rest-discounts`:
|
||||
|
||||
Discounts
|
||||
=========
|
||||
|
||||
Resource description
|
||||
--------------------
|
||||
|
||||
Discounts provide a way to automatically reduce the price of a cart if it matches a given set of conditions.
|
||||
Discounts are available to everyone. If you want to give a discount just to specific persons, look at
|
||||
:ref:`vouchers <rest-vouchers>` instead. If you are interested in the behind-the-scenes details of how
|
||||
discounts are calculated for a specific order, have a look at :ref:`our algorithm documentation <algorithms-pricing>`.
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
======================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
======================================== ========================== =======================================================
|
||||
id integer Internal ID of the discount rule
|
||||
active boolean The discount will be ignored if this is ``false``
|
||||
internal_name string A name for the rule used in the backend
|
||||
position integer An integer, used for sorting the rules which are applied in order
|
||||
sales_channels list of strings Sales channels this discount is available on, such as
|
||||
``"web"`` or ``"resellers"``. Defaults to ``["web"]``.
|
||||
available_from datetime The first date time at which this discount can be applied
|
||||
(or ``null``).
|
||||
available_until datetime The last date time at which this discount can be applied
|
||||
(or ``null``).
|
||||
subevent_mode strings Determines how the discount is handled when used in an
|
||||
event series. Can be ``"mixed"`` (no special effect),
|
||||
``"same"`` (discount is only applied for groups within
|
||||
the same date), or ``"distinct"`` (discount is only applied
|
||||
for groups with no two same dates).
|
||||
condition_all_products boolean If ``true``, the discount applies to all items.
|
||||
condition_limit_products list of integers If ``condition_all_products`` is not set, this is a list
|
||||
of internal item IDs that the discount applies to.
|
||||
condition_apply_to_addons boolean If ``true``, the discount applies to add-on products as well,
|
||||
otherwise it only applies to top-level items. The discount never
|
||||
applies to bundled products.
|
||||
condition_ignore_voucher_discounted boolean If ``true``, the discount does not apply to products which have
|
||||
been discounted by a voucher.
|
||||
condition_min_count integer The minimum number of matching products for the discount
|
||||
to be activated.
|
||||
condition_min_value money (string) The minimum value of matching products for the discount
|
||||
to be activated. Cannot be combined with ``condition_min_count``,
|
||||
or with ``subevent_mode`` set to ``distinct``.
|
||||
benefit_discount_matching_percent decimal (string) The percentage of price reduction for matching products.
|
||||
benefit_only_apply_to_cheapest_n_matches integer If set higher than 0, the discount will only be applied to
|
||||
the cheapest matches. Useful for a "3 for 2"-style discount.
|
||||
Cannot be combined with ``condition_min_value``.
|
||||
======================================== ========================== =======================================================
|
||||
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/discounts/
|
||||
|
||||
Returns a list of all discounts within a given event.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/discounts/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"active": true,
|
||||
"internal_name": "3 for 2",
|
||||
"position": 1,
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"subevent_mode": "mixed",
|
||||
"condition_all_products": true,
|
||||
"condition_limit_products": [],
|
||||
"condition_apply_to_addons": true,
|
||||
"condition_ignore_voucher_discounted": false,
|
||||
"condition_min_count": 3,
|
||||
"condition_min_value": "0.00",
|
||||
"benefit_discount_matching_percent": "100.00",
|
||||
"benefit_only_apply_to_cheapest_n_matches": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:query boolean active: If set to ``true`` or ``false``, only discounts with this value for the field ``active`` will be
|
||||
returned.
|
||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``id`` and ``position``.
|
||||
Default: ``position``
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/discounts/(id)/
|
||||
|
||||
Returns information on one discount, identified by its ID.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/discounts/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"active": true,
|
||||
"internal_name": "3 for 2",
|
||||
"position": 1,
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"subevent_mode": "mixed",
|
||||
"condition_all_products": true,
|
||||
"condition_limit_products": [],
|
||||
"condition_apply_to_addons": true,
|
||||
"condition_ignore_voucher_discounted": false,
|
||||
"condition_min_count": 3,
|
||||
"condition_min_value": "0.00",
|
||||
"benefit_discount_matching_percent": "100.00",
|
||||
"benefit_only_apply_to_cheapest_n_matches": 1
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:param id: The ``id`` field of the discount to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/discounts/
|
||||
|
||||
Creates a new discount
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/discounts/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"active": true,
|
||||
"internal_name": "3 for 2",
|
||||
"position": 1,
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"subevent_mode": "mixed",
|
||||
"condition_all_products": true,
|
||||
"condition_limit_products": [],
|
||||
"condition_apply_to_addons": true,
|
||||
"condition_ignore_voucher_discounted": false,
|
||||
"condition_min_count": 3,
|
||||
"condition_min_value": "0.00",
|
||||
"benefit_discount_matching_percent": "100.00",
|
||||
"benefit_only_apply_to_cheapest_n_matches": 1
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"active": true,
|
||||
"internal_name": "3 for 2",
|
||||
"position": 1,
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"subevent_mode": "mixed",
|
||||
"condition_all_products": true,
|
||||
"condition_limit_products": [],
|
||||
"condition_apply_to_addons": true,
|
||||
"condition_ignore_voucher_discounted": false,
|
||||
"condition_min_count": 3,
|
||||
"condition_min_value": "0.00",
|
||||
"benefit_discount_matching_percent": "100.00",
|
||||
"benefit_only_apply_to_cheapest_n_matches": 1
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer of the event to create a discount for
|
||||
:param event: The ``slug`` field of the event to create a discount for
|
||||
:statuscode 201: no error
|
||||
:statuscode 400: The discount could not be created due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
|
||||
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/discounts/(id)/
|
||||
|
||||
Update a discount. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
|
||||
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
|
||||
want to change.
|
||||
|
||||
You can change all fields of the resource except the ``id`` field.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/bigevents/events/sampleconf/discounts/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 94
|
||||
|
||||
{
|
||||
"active": false
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"active": false,
|
||||
"internal_name": "3 for 2",
|
||||
"position": 1,
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"subevent_mode": "mixed",
|
||||
"condition_all_products": true,
|
||||
"condition_limit_products": [],
|
||||
"condition_apply_to_addons": true,
|
||||
"condition_ignore_voucher_discounted": false,
|
||||
"condition_min_count": 3,
|
||||
"condition_min_value": "0.00",
|
||||
"benefit_discount_matching_percent": "100.00",
|
||||
"benefit_only_apply_to_cheapest_n_matches": 1
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param event: The ``slug`` field of the event to modify
|
||||
:param id: The ``id`` field of the discount to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The discount could not be modified due to invalid submitted data
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
|
||||
|
||||
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/discount/(id)/
|
||||
|
||||
Delete a discount.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v1/organizers/bigevents/events/sampleconf/discount/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 discount to delete
|
||||
:statuscode 204: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
|
||||
@@ -52,34 +52,9 @@ sales_channels list A list of sales
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
.. versionchanged:: 3.3
|
||||
|
||||
The attributes ``geo_lat`` and ``geo_lon`` have been added.
|
||||
|
||||
.. versionchanged:: 3.4
|
||||
|
||||
The attribute ``timezone`` has been added.
|
||||
|
||||
.. versionchanged:: 3.7
|
||||
|
||||
The attribute ``item_meta_properties`` has been added.
|
||||
|
||||
.. versionchanged:: 3.12
|
||||
|
||||
The attribute ``valid_keys`` has been added.
|
||||
|
||||
.. versionchanged:: 3.14
|
||||
|
||||
The attribute ``sales_channels`` has been added.
|
||||
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. versionchanged:: 3.3
|
||||
|
||||
The events resource can now be filtered by meta data attributes.
|
||||
|
||||
.. versionchanged:: 4.0
|
||||
|
||||
The ``clone_from`` parameter has been added to the event creation endpoint.
|
||||
@@ -567,10 +542,6 @@ information about the properties.
|
||||
.. warning:: This API is intended for advanced users. Even though we take care to validate your input, you will be
|
||||
able to break your event using this API by creating situations of conflicting settings. Please take care.
|
||||
|
||||
.. versionchanged:: 3.6
|
||||
|
||||
Initial support for settings has been added to the API.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/settings/
|
||||
|
||||
Get current values of event settings.
|
||||
|
||||
@@ -6,10 +6,6 @@ Data exporters
|
||||
pretix and it's plugins include a number of data exporters that allow you to bulk download various data from pretix in
|
||||
different formats. This page shows you how to use these exporters through the API.
|
||||
|
||||
.. versionchanged:: 3.13
|
||||
|
||||
This feature has been added to the API.
|
||||
|
||||
.. warning::
|
||||
|
||||
While we consider the methods listed on this page to be a stable API, the availability and specific input field
|
||||
@@ -182,7 +178,7 @@ endpoints:
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"download": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/orderlist/download/29891ede-196f-4942-9e26-d055a36e98b8/3f279f13-c198-4137-b49b-9b360ce9fcce/"
|
||||
"download": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/exporters/orderlist/download/29891ede-196f-4942-9e26-d055a36e98b8/3f279f13-c198-4137-b49b-9b360ce9fcce/"
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
|
||||
@@ -40,10 +40,6 @@ text string Custom text of
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. versionadded:: 3.14
|
||||
|
||||
The transaction list endpoint was added.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/giftcards/
|
||||
|
||||
Returns a list of all gift cards issued by a given organizer.
|
||||
@@ -257,10 +253,6 @@ Endpoints
|
||||
"value": "15.37"
|
||||
}
|
||||
|
||||
.. versionchanged:: 3.5
|
||||
|
||||
This endpoint now returns status code ``409`` if the transaction would lead to a negative gift card value.
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param id: The ``id`` field of the gift card to modify
|
||||
:query boolean include_accepted: Also show gift cards issued by other organizers that are accepted by this organizer.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,6 +24,8 @@ Resources and endpoints
|
||||
orders
|
||||
invoices
|
||||
vouchers
|
||||
discounts
|
||||
checkin
|
||||
checkinlists
|
||||
waitinglist
|
||||
customers
|
||||
@@ -31,6 +38,7 @@ Resources and endpoints
|
||||
webhooks
|
||||
seatingplans
|
||||
exporters
|
||||
shredders
|
||||
sendmail_rules
|
||||
billing_invoices
|
||||
billing_var
|
||||
billing_var
|
||||
@@ -58,6 +58,12 @@ 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
|
||||
@@ -102,16 +108,6 @@ internal_reference string Customer's refe
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
.. versionchanged:: 3.4
|
||||
|
||||
The attribute ``lines.number`` has been added.
|
||||
|
||||
.. versionchanged:: 3.17
|
||||
|
||||
The attribute ``invoice_to_*``, ``invoice_from_*``, ``custom_field``, ``lines.item``, ``lines.variation``, ``lines.event_date_from``,
|
||||
``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.
|
||||
@@ -120,6 +116,10 @@ internal_reference string Customer's refe
|
||||
|
||||
The attribute ``lines.event_location`` has been added.
|
||||
|
||||
.. versionchanged:: 4.6
|
||||
|
||||
The attribute ``lines.subevent`` has been added.
|
||||
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
@@ -185,6 +185,7 @@ 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",
|
||||
@@ -274,6 +275,7 @@ 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",
|
||||
|
||||
@@ -24,6 +24,9 @@ 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.
|
||||
@@ -40,8 +43,13 @@ available_until datetime The last date t
|
||||
hide_without_voucher boolean If ``true``, this variation is only shown during the voucher
|
||||
redemption process, but not in the normal shop
|
||||
frontend.
|
||||
meta_data object Values set for event-specific meta data parameters.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 4.16
|
||||
|
||||
The ``meta_data`` attribute has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -76,6 +84,7 @@ Endpoints
|
||||
"en": "S"
|
||||
},
|
||||
"active": true,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
@@ -90,6 +99,7 @@ Endpoints
|
||||
"default_price": "223.00",
|
||||
"price": 223.0,
|
||||
"original_price": null,
|
||||
"meta_data": {}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
@@ -97,13 +107,15 @@ Endpoints
|
||||
"en": "L"
|
||||
},
|
||||
"active": true,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
"description": {},
|
||||
"position": 1,
|
||||
"default_price": null,
|
||||
"price": 15.0
|
||||
"price": 15.0,
|
||||
"meta_data": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -147,6 +159,7 @@ Endpoints
|
||||
"price": "10.00",
|
||||
"original_price": null,
|
||||
"active": true,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
@@ -155,7 +168,8 @@ Endpoints
|
||||
"available_until": null,
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"position": 0
|
||||
"position": 0,
|
||||
"meta_data": {}
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
@@ -183,6 +197,7 @@ Endpoints
|
||||
"value": {"en": "Student"},
|
||||
"default_price": "10.00",
|
||||
"active": true,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
@@ -191,7 +206,8 @@ Endpoints
|
||||
"available_until": null,
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"position": 0
|
||||
"position": 0,
|
||||
"meta_data": {}
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@@ -209,6 +225,7 @@ Endpoints
|
||||
"price": "10.00",
|
||||
"original_price": null,
|
||||
"active": true,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
@@ -217,7 +234,8 @@ Endpoints
|
||||
"available_until": null,
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"position": 0
|
||||
"position": 0,
|
||||
"meta_data": {}
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer of the event/item to create a variation for
|
||||
@@ -266,6 +284,7 @@ Endpoints
|
||||
"price": "10.00",
|
||||
"original_price": null,
|
||||
"active": false,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
@@ -274,7 +293,8 @@ Endpoints
|
||||
"available_until": null,
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"position": 1
|
||||
"position": 1,
|
||||
"meta_data": {}
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
|
||||
@@ -123,6 +123,7 @@ variations list of objects A list with one
|
||||
├ hide_without_voucher boolean If ``true``, this variation is only shown during the voucher
|
||||
redemption process, but not in the normal shop
|
||||
frontend.
|
||||
├ meta_data object Values set for event-specific meta data parameters.
|
||||
└ 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,
|
||||
@@ -146,14 +147,6 @@ bundles list of objects Definition of b
|
||||
meta_data object Values set for event-specific meta data parameters.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 3.7
|
||||
|
||||
The attribute ``meta_data`` has been added.
|
||||
|
||||
.. versionchanged:: 3.10
|
||||
|
||||
The attribute ``multi_allowed`` has been added to ``addons``.
|
||||
|
||||
.. versionchanged:: 4.0
|
||||
|
||||
The attributes ``require_membership``, ``require_membership_types``, ``grant_membership_type``, ``grant_membership_duration_like_event``,
|
||||
@@ -163,6 +156,10 @@ meta_data object Values set for
|
||||
|
||||
The attributes ``require_membership_hidden`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 4.16
|
||||
|
||||
The ``variations[x].meta_data`` attribute has been added.
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
@@ -255,6 +252,7 @@ Endpoints
|
||||
"available_until": null,
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"meta_data": {},
|
||||
"position": 0
|
||||
},
|
||||
{
|
||||
@@ -270,6 +268,7 @@ Endpoints
|
||||
"available_until": null,
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"meta_data": {},
|
||||
"position": 1
|
||||
}
|
||||
],
|
||||
@@ -369,6 +368,7 @@ Endpoints
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"hide_without_voucher": false,
|
||||
"meta_data": {},
|
||||
"position": 0
|
||||
},
|
||||
{
|
||||
@@ -384,6 +384,7 @@ Endpoints
|
||||
"available_until": null,
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"meta_data": {},
|
||||
"position": 1
|
||||
}
|
||||
],
|
||||
@@ -463,6 +464,7 @@ Endpoints
|
||||
"available_until": null,
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"meta_data": {},
|
||||
"position": 0
|
||||
},
|
||||
{
|
||||
@@ -478,6 +480,7 @@ Endpoints
|
||||
"available_until": null,
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"meta_data": {},
|
||||
"position": 1
|
||||
}
|
||||
],
|
||||
@@ -546,6 +549,7 @@ Endpoints
|
||||
"available_until": null,
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"meta_data": {},
|
||||
"position": 0
|
||||
},
|
||||
{
|
||||
@@ -561,6 +565,7 @@ Endpoints
|
||||
"available_until": null,
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"meta_data": {},
|
||||
"position": 1
|
||||
}
|
||||
],
|
||||
@@ -660,6 +665,7 @@ Endpoints
|
||||
"available_until": null,
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"meta_data": {},
|
||||
"position": 0
|
||||
},
|
||||
{
|
||||
@@ -675,6 +681,7 @@ Endpoints
|
||||
"available_until": null,
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"meta_data": {},
|
||||
"position": 1
|
||||
}
|
||||
],
|
||||
|
||||
@@ -60,6 +60,7 @@ invoice_address object Invoice address
|
||||
├ state string Customer state (ISO 3166-2 code). Only supported in
|
||||
AU, BR, CA, CN, MY, MX, and US.
|
||||
├ internal_reference string Customer's internal reference to be printed on the invoice
|
||||
├ custom_field string Custom invoice address field
|
||||
├ vat_id string Customer VAT ID
|
||||
└ vat_id_validated string ``true``, if the VAT ID has been validated against the
|
||||
EU VAT service and validation was successful. This only
|
||||
@@ -68,6 +69,7 @@ positions list of objects List of order p
|
||||
non-canceled positions are included.
|
||||
fees list of objects List of fees included in the order total. By default, only
|
||||
non-canceled fees are included.
|
||||
├ id integer Internal ID of the fee record
|
||||
├ fee_type string Type of fee (currently ``payment``, ``passbook``,
|
||||
``other``)
|
||||
├ value money (string) Fee amount
|
||||
@@ -96,30 +98,6 @@ last_modified datetime Last modificati
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
.. versionchanged:: 3.5
|
||||
|
||||
The ``order.fees.canceled`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 3.8
|
||||
|
||||
The ``reactivate`` operation has been added.
|
||||
|
||||
.. versionchanged:: 3.10
|
||||
|
||||
The ``search`` query parameter has been added.
|
||||
|
||||
.. versionchanged:: 3.11
|
||||
|
||||
The ``exclude`` and ``subevent_after`` query parameter has been added.
|
||||
|
||||
.. versionchanged:: 3.13
|
||||
|
||||
The ``subevent_before`` query parameter has been added.
|
||||
|
||||
.. versionchanged:: 3.14
|
||||
|
||||
The ``phone`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 4.0
|
||||
|
||||
The ``customer`` attribute has been added.
|
||||
@@ -132,6 +110,18 @@ last_modified datetime Last modificati
|
||||
|
||||
The ``item`` and ``variation`` query parameters have been added.
|
||||
|
||||
.. versionchanged:: 4.6
|
||||
|
||||
The ``subevent`` query parameters has been added.
|
||||
|
||||
.. versionchanged:: 4.8
|
||||
|
||||
The ``order.fees.id`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 4.15
|
||||
|
||||
The ``include`` query parameter has been added.
|
||||
|
||||
|
||||
.. _order-position-resource:
|
||||
|
||||
@@ -168,6 +158,7 @@ tax_rule integer The ID of the u
|
||||
secret string Secret code printed on the tickets for validation
|
||||
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``).
|
||||
discount integer ID of a discount that has been used during the creation of this position in some way (or ``null``).
|
||||
pseudonymization_id string A random ID, e.g. for use in lead scanning apps
|
||||
checkins list of objects List of **successful** check-ins with this ticket
|
||||
├ id integer Internal ID of the check-in event
|
||||
@@ -195,27 +186,6 @@ pdf_data object Data object req
|
||||
``pdf_data=true`` query parameter to your request.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 3.3
|
||||
|
||||
The ``url`` of a ticket ``download`` can now also return a ``text/uri-list`` instead of a file. See
|
||||
:ref:`order-position-ticket-download` for details.
|
||||
|
||||
.. versionchanged:: 3.5
|
||||
|
||||
The attribute ``canceled`` has been added.
|
||||
|
||||
.. versionchanged:: 3.8
|
||||
|
||||
The attributes ``company``, ``street``, ``zipcode``, ``city``, ``country``, and ``state`` have been added.
|
||||
|
||||
.. versionchanged:: 3.9
|
||||
|
||||
The ``checkin.type`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 3.16
|
||||
|
||||
Answers to file questions are now returned as an URL.
|
||||
|
||||
.. _order-payment-resource:
|
||||
|
||||
Order payment resource
|
||||
@@ -262,15 +232,20 @@ created datetime Date and time o
|
||||
comment string Reason for refund (shown to the customer in some cases, can be ``null``).
|
||||
execution_date datetime Date and time of completion of this refund (or ``null``)
|
||||
provider string Identification string of the payment provider
|
||||
details object Refund-specific information. This is a dictionary
|
||||
with various fields that can be different between
|
||||
payment providers, versions, payment states, etc. If
|
||||
you read this field, you always need to be able to
|
||||
deal with situations where values that you expect are
|
||||
missing. Mostly, the field contains various IDs that
|
||||
can be used for matching with other systems. If a
|
||||
payment provider does not implement this feature,
|
||||
the object is empty.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
List of all orders
|
||||
------------------
|
||||
|
||||
.. versionchanged:: 3.5
|
||||
|
||||
The ``include_canceled_positions`` and ``include_canceled_fees`` query parameters have been added.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/
|
||||
|
||||
Returns a list of all orders within a given event.
|
||||
@@ -361,6 +336,7 @@ List of all orders
|
||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||
"addon_to": null,
|
||||
"subevent": null,
|
||||
"discount": null,
|
||||
"pseudonymization_id": "MQLJvANO3B",
|
||||
"seat": null,
|
||||
"checkins": [
|
||||
@@ -418,7 +394,7 @@ List of all orders
|
||||
``last_modified``, and ``status``. Default: ``datetime``
|
||||
: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 string search: Only return orders matching a given search query (matching for names, email addresses, and company names)
|
||||
: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``
|
||||
@@ -433,9 +409,11 @@ 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.
|
||||
:query string include: Include only the given field in the output, e.g. ``fees`` or ``positions.downloads``. Can be used as a performance optimization. Can be passed multiple times. ``include`` is applied before ``exclude``, so ``exclude`` takes precedence.
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:resheader X-Page-Generated: The server time at the beginning of the operation. If you're using this API to fetch
|
||||
@@ -447,10 +425,6 @@ List of all orders
|
||||
Fetching individual orders
|
||||
--------------------------
|
||||
|
||||
.. versionchanged:: 3.5
|
||||
|
||||
The ``include_canceled_positions`` and ``include_canceled_fees`` query parameters have been added.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/
|
||||
|
||||
Returns information on one order, identified by its order code.
|
||||
@@ -535,6 +509,7 @@ Fetching individual orders
|
||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||
"addon_to": null,
|
||||
"subevent": null,
|
||||
"discount": null,
|
||||
"pseudonymization_id": "MQLJvANO3B",
|
||||
"seat": null,
|
||||
"checkins": [
|
||||
@@ -599,13 +574,17 @@ Fetching individual orders
|
||||
Order ticket download
|
||||
---------------------
|
||||
|
||||
.. versionchanged:: 4.10
|
||||
|
||||
The API now supports ticket downloads for pending orders if allowed by the event settings.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/download/(output)/
|
||||
|
||||
Download tickets for an order, identified by its order code. Depending on the chosen output, the response might
|
||||
be a ZIP file, PDF file or something else. The order details response contains a list of output options for this
|
||||
particular order.
|
||||
|
||||
Tickets can be only downloaded if the order is paid and if ticket downloads are active. Note that in some cases the
|
||||
Tickets can only be downloaded if ticket downloads are active and – depending on event settings – the order is either paid or pending. Note that in some cases the
|
||||
ticket file might not yet have been created. In that case, you will receive a status code :http:statuscode:`409` and
|
||||
you are expected to retry the request after a short period of waiting.
|
||||
|
||||
@@ -730,6 +709,37 @@ Generating new secrets
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to update this order.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/regenerate_secrets/
|
||||
|
||||
Triggers generation of a new ``secret`` attribute for a single order position.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/orderpositions/23/regenerate_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
|
||||
|
||||
(Full order position resource, see above.)
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer of the event
|
||||
:param event: The ``slug`` field of the event
|
||||
:param code: The ``id`` field of the order position to update
|
||||
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The order position could not be updated due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to update this order position.
|
||||
|
||||
Deleting orders
|
||||
---------------
|
||||
|
||||
@@ -807,7 +817,7 @@ Creating orders
|
||||
|
||||
You can supply the following fields of the resource:
|
||||
|
||||
* ``code`` (optional)
|
||||
* ``code`` (optional) – Only ``A-Z`` and ``0-9``, but without ``O`` and ``1``.
|
||||
* ``status`` (optional) – Defaults to pending for non-free orders and paid for free orders. You can only set this to
|
||||
``"n"`` for pending or ``"p"`` for paid. We will create a payment object for this order either in state ``created``
|
||||
or in state ``confirmed``, depending on this value. If you create a paid order, the ``order_paid`` signal will
|
||||
@@ -834,6 +844,7 @@ Creating orders
|
||||
* ``comment`` (optional)
|
||||
* ``custom_followup_at`` (optional)
|
||||
* ``checkin_attention`` (optional)
|
||||
* ``require_approval`` (optional)
|
||||
* ``invoice_address`` (optional)
|
||||
|
||||
* ``company``
|
||||
@@ -893,8 +904,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
|
||||
@@ -987,10 +999,6 @@ Creating orders
|
||||
Order state operations
|
||||
----------------------
|
||||
|
||||
.. versionchanged:: 3.12
|
||||
|
||||
The ``mark_paid`` operation now takes a ``send_email`` parameter.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/mark_paid/
|
||||
|
||||
Marks a pending or expired order as successfully paid.
|
||||
@@ -1039,6 +1047,9 @@ Order state operations
|
||||
will instead stay paid, but all positions will be removed (or marked as canceled) and replaced by the cancellation
|
||||
fee as the only component of the order.
|
||||
|
||||
You can control whether the customer is notified through ``send_email`` (defaults to ``true``).
|
||||
You can pass a ``comment`` that can be visible to the user if it is used in the email template.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
@@ -1050,6 +1061,7 @@ Order state operations
|
||||
|
||||
{
|
||||
"send_email": true,
|
||||
"comment": "Event was canceled.",
|
||||
"cancellation_fee": null
|
||||
}
|
||||
|
||||
@@ -1388,10 +1400,6 @@ Sending e-mails
|
||||
List of all order positions
|
||||
---------------------------
|
||||
|
||||
.. versionchanged:: 3.5
|
||||
|
||||
The ``include_canceled_positions`` and ``include_canceled_fees`` query parameters have been added.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/
|
||||
|
||||
Returns a list of all order positions within a given event.
|
||||
@@ -1435,6 +1443,7 @@ List of all order positions
|
||||
"tax_rule": null,
|
||||
"tax_value": "0.00",
|
||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||
"discount": null,
|
||||
"pseudonymization_id": "MQLJvANO3B",
|
||||
"seat": null,
|
||||
"addon_to": null,
|
||||
@@ -1545,6 +1554,7 @@ Fetching individual positions
|
||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||
"addon_to": null,
|
||||
"subevent": null,
|
||||
"discount": null,
|
||||
"pseudonymization_id": "MQLJvANO3B",
|
||||
"seat": null,
|
||||
"checkins": [
|
||||
@@ -1588,6 +1598,10 @@ Fetching individual positions
|
||||
Order position ticket download
|
||||
------------------------------
|
||||
|
||||
.. versionchanged:: 4.10
|
||||
|
||||
The API now supports ticket downloads for pending orders if allowed by the event settings.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/download/(output)/
|
||||
|
||||
Download tickets for one order position, identified by its internal ID.
|
||||
@@ -1599,7 +1613,7 @@ Order position ticket download
|
||||
The referenced URL can provide a download or a regular, human-viewable website - so it is advised to open this URL
|
||||
in a webbrowser and leave it up to the user to handle the result.
|
||||
|
||||
Tickets can be only downloaded if the order is paid and if ticket downloads are active. Also, depending on event
|
||||
Tickets can only be downloaded if ticket downloads are active and – depending on event settings – the order is either paid or pending. Also, depending on event
|
||||
configuration downloads might be only unavailable for add-on products or non-admission products.
|
||||
Note that in some cases the ticket file might not yet have been created. In that case, you will receive a status
|
||||
code :http:statuscode:`409` and you are expected to retry the request after a short period of waiting.
|
||||
@@ -1635,12 +1649,15 @@ Order position ticket download
|
||||
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting for a few
|
||||
seconds.
|
||||
|
||||
.. _rest-orderpositions-manipulate:
|
||||
|
||||
Manipulating individual positions
|
||||
---------------------------------
|
||||
|
||||
.. versionchanged:: 3.15
|
||||
.. versionchanged:: 4.8
|
||||
|
||||
The ``PATCH`` method has been added for individual positions.
|
||||
The ``PATCH`` method now supports changing items, variations, subevents, seats, prices, and tax rules.
|
||||
The ``POST`` endpoint to add individual positions has been added.
|
||||
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/
|
||||
|
||||
@@ -1668,6 +1685,21 @@ Manipulating individual positions
|
||||
and ``option_identifiers`` will be ignored. As a special case, you can submit the magic value
|
||||
``"file:keep"`` as the answer to a file question to keep the current value without re-uploading it.
|
||||
|
||||
* ``item``
|
||||
|
||||
* ``variation``
|
||||
|
||||
* ``subevent``
|
||||
|
||||
* ``seat`` (specified as a string mapping to a ``string_guid``)
|
||||
|
||||
* ``price``
|
||||
|
||||
* ``tax_rule``
|
||||
|
||||
Changing parameters such as ``item`` or ``price`` will **not** automatically trigger creation of a new invoice,
|
||||
you need to take care of that yourself.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
@@ -1689,7 +1721,7 @@ Manipulating individual positions
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
(Full order resource, see above.)
|
||||
(Full order position resource, see above.)
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer of the event
|
||||
:param event: The ``slug`` field of the event
|
||||
@@ -1700,9 +1732,83 @@ Manipulating individual positions
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to update this order.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/
|
||||
|
||||
Adds a new position to an order. Currently, only the following fields are supported:
|
||||
|
||||
* ``order`` (mandatory, specified as a string mapping to a ``code``)
|
||||
|
||||
* ``addon_to`` (optional, specified as an integer mapping to the ``positionid`` of the parent position)
|
||||
|
||||
* ``item`` (mandatory)
|
||||
|
||||
* ``variation`` (mandatory depending on item)
|
||||
|
||||
* ``subevent`` (mandatory depending on event)
|
||||
|
||||
* ``seat`` (specified as a string mapping to a ``string_guid``, mandatory depending on event and item)
|
||||
|
||||
* ``price`` (default price will be used if unset)
|
||||
|
||||
* ``attendee_email``
|
||||
|
||||
* ``attendee_name_parts`` or ``attendee_name``
|
||||
|
||||
* ``company``
|
||||
|
||||
* ``street``
|
||||
|
||||
* ``zipcode``
|
||||
|
||||
* ``city``
|
||||
|
||||
* ``country``
|
||||
|
||||
* ``state``
|
||||
|
||||
* ``answers``: Validation is handled the same way as when creating orders through the API. You are therefore
|
||||
expected to provide ``question``, ``answer``, and possibly ``options``. ``question_identifier``
|
||||
and ``option_identifiers`` will be ignored. As a special case, you can submit the magic value
|
||||
``"file:keep"`` as the answer to a file question to keep the current value without re-uploading it.
|
||||
|
||||
This will **not** automatically trigger creation of a new invoice, you need to take care of that yourself.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/orderpositions/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"order": "ABC12",
|
||||
"item": 5,
|
||||
"addon_to": 1
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
(Full order position resource, see above.)
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer of the event
|
||||
:param event: The ``slug`` field of the event
|
||||
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The position could not be created due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this position.
|
||||
|
||||
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/
|
||||
|
||||
Deletes an order position, identified by its internal ID.
|
||||
Cancels an order position, identified by its internal ID.
|
||||
|
||||
**Example request**:
|
||||
|
||||
@@ -1728,18 +1834,132 @@ Manipulating individual positions
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
:statuscode 404: The requested order position does not exist.
|
||||
|
||||
Changing order contents
|
||||
-----------------------
|
||||
|
||||
While you can :ref:`change positions individually <rest-orderpositions-manipulate>` sometimes it is necessary to make
|
||||
multiple changes to an order at once within one transaction. This makes it possible to e.g. swap the seats of two
|
||||
attendees in an order without running into conflicts. This interface also offers some possibilities not available
|
||||
otherwise, such as splitting an order or changing fees.
|
||||
|
||||
.. versionchanged:: 4.8
|
||||
|
||||
This endpoint has been added to the system.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/change/
|
||||
|
||||
Performs a change operation on an order. You can supply the following fields:
|
||||
|
||||
* ``patch_positions``: A list of objects with the two keys ``position`` specifying an order position ID and
|
||||
``body`` specifying the desired changed values of the position (``item``, ``variation``, ``subevent``, ``seat``,
|
||||
``price``, ``tax_rule``).
|
||||
|
||||
* ``cancel_positions``: A list of objects with the single key ``position`` specifying an order position ID.
|
||||
|
||||
* ``split_positions``: A list of objects with the single key ``position`` specifying an order position ID.
|
||||
|
||||
* ``create_positions``: A list of objects describing new order positions with the same fields supported as when
|
||||
creating them individually through the ``POST …/orderpositions/`` endpoint.
|
||||
|
||||
* ``patch_fees``: A list of objects with the two keys ``fee`` specifying an order fee ID and
|
||||
``body`` specifying the desired changed values of the position (``value``).
|
||||
|
||||
* ``cancel_fees``: A list of objects with the single key ``fee`` specifying an order fee ID.
|
||||
|
||||
* ``recalculate_taxes``: If set to ``"keep_net"``, all taxes will be recalculated based on the tax rule and invoice
|
||||
address, the net price will be kept. If set to ``"keep_gross"``, the gross price will be kept. If set to ``null``
|
||||
(the default) the taxes are not recalculated.
|
||||
|
||||
* ``send_email``: If set to ``true``, the customer will be notified about the change. Defaults to ``false``.
|
||||
|
||||
* ``reissue_invoice``: If set to ``true`` and an invoice exists for the order, it will be canceled and a new invoice
|
||||
will be issued. Defaults to ``true``.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/change/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"cancel_positions": [
|
||||
{
|
||||
"position": 12373
|
||||
}
|
||||
],
|
||||
"patch_positions": [
|
||||
{
|
||||
"position": 12374,
|
||||
"body": {
|
||||
"item": 12,
|
||||
"variation": None,
|
||||
"subevent": 562,
|
||||
"seat": "seat-guid-2",
|
||||
"price": "99.99",
|
||||
"tax_rule": 15
|
||||
}
|
||||
}
|
||||
],
|
||||
"split_positions": [
|
||||
{
|
||||
"position": 12375
|
||||
}
|
||||
],
|
||||
"create_positions": [
|
||||
{
|
||||
"item": 12,
|
||||
"variation": None,
|
||||
"subevent": 562,
|
||||
"seat": "seat-guid-2",
|
||||
"price": "99.99",
|
||||
"addon_to": 12374,
|
||||
"attendee_name": "Peter",
|
||||
}
|
||||
],
|
||||
"cancel_fees": [
|
||||
{
|
||||
"fee": 49
|
||||
}
|
||||
],
|
||||
"change_fees": [
|
||||
{
|
||||
"fee": 51,
|
||||
"body": {
|
||||
"value": "12.00"
|
||||
}
|
||||
}
|
||||
],
|
||||
"reissue_invoice": true,
|
||||
"send_email": true,
|
||||
"recalculate_taxes": "keep_gross"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
(Full order position resource, see above.)
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer of the event
|
||||
:param event: The ``slug`` field of the event
|
||||
:param code: The ``code`` field of the order to update
|
||||
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The order could not be updated due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to update this order.
|
||||
|
||||
|
||||
Order payment endpoints
|
||||
-----------------------
|
||||
|
||||
.. versionchanged:: 3.6
|
||||
|
||||
Payments can now be created through the API.
|
||||
|
||||
.. versionchanged:: 3.12
|
||||
|
||||
The ``confirm`` operation now takes a ``send_email`` parameter.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/payments/
|
||||
|
||||
Returns a list of all payments for an order.
|
||||
@@ -2045,6 +2265,7 @@ Order refund endpoints
|
||||
"created": "2017-12-01T10:00:00Z",
|
||||
"execution_date": "2017-12-04T12:13:12Z",
|
||||
"comment": "Cancellation",
|
||||
"details": {},
|
||||
"provider": "banktransfer"
|
||||
}
|
||||
]
|
||||
@@ -2088,6 +2309,7 @@ Order refund endpoints
|
||||
"created": "2017-12-01T10:00:00Z",
|
||||
"execution_date": "2017-12-04T12:13:12Z",
|
||||
"comment": "Cancellation",
|
||||
"details": {},
|
||||
"provider": "banktransfer"
|
||||
}
|
||||
|
||||
@@ -2145,6 +2367,7 @@ Order refund endpoints
|
||||
"created": "2017-12-01T10:00:00Z",
|
||||
"execution_date": null,
|
||||
"comment": "Cancellation",
|
||||
"details": {},
|
||||
"provider": "manual"
|
||||
}
|
||||
|
||||
@@ -2274,10 +2497,6 @@ Revoked ticket secrets
|
||||
|
||||
With some non-default ticket secret generation methods, a list of revoked ticket secrets is required for proper validation.
|
||||
|
||||
.. versionchanged:: 3.12
|
||||
|
||||
Added revocation lists.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/revokedsecrets/
|
||||
|
||||
Returns a list of all revoked secrets within a given event.
|
||||
|
||||
@@ -109,10 +109,6 @@ information about the properties.
|
||||
.. warning:: This API is intended for advanced users. Even though we take care to validate your input, you will be
|
||||
able to break your shops using this API by creating situations of conflicting settings. Please take care.
|
||||
|
||||
.. versionchanged:: 3.14
|
||||
|
||||
Initial support for settings has been added to the API.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/settings/
|
||||
|
||||
Get current values of organizer settings.
|
||||
|
||||
@@ -76,26 +76,9 @@ dependency_value string An old version
|
||||
for one value. **Deprecated.**
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 3.5
|
||||
|
||||
The attribute ``help_text`` has been added.
|
||||
|
||||
.. versionchanged:: 3.14
|
||||
|
||||
The attributes ``valid_*`` have been added.
|
||||
|
||||
.. versionchanged:: 3.18
|
||||
|
||||
The attribute ``valid_file_portrait`` have been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. versionchanged:: 1.15
|
||||
|
||||
The questions endpoint has been extended by the filter queries ``ask_during_checkin``, ``requred``, and
|
||||
``identifier``.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/questions/
|
||||
|
||||
Returns a list of all questions within a given event.
|
||||
|
||||
@@ -36,10 +36,6 @@ available_number integer Number of avail
|
||||
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.
|
||||
@@ -89,7 +85,8 @@ Endpoints
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``id`` and ``position``.
|
||||
Default: ``position``
|
||||
:query integer subevent: Only return quotas of the sub-event with the given ID
|
||||
:query integer subevent: Only return quotas of the sub-event with the given ID.
|
||||
:query integer subevent__in: Only return quotas of sub-events with one the given IDs (comma-separated).
|
||||
: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
|
||||
|
||||
177
doc/api/resources/shredders.rst
Normal file
@@ -0,0 +1,177 @@
|
||||
.. spelling:: checkin
|
||||
|
||||
Data shredders
|
||||
==============
|
||||
|
||||
pretix and it's plugins include a number of data shredders that allow you to clear personal information from the system.
|
||||
This page shows you how to use these shredders through the API.
|
||||
|
||||
.. versionchanged:: 4.12
|
||||
|
||||
This feature has been added to the API.
|
||||
|
||||
.. warning::
|
||||
|
||||
Unlike the user interface, the API will not force you to download tax-relevant data before you delete it.
|
||||
|
||||
Listing available shredders
|
||||
---------------------------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/shredders/
|
||||
|
||||
Returns a list of all exporters shredders for a given event.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/shredders/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"identifier": "question_answers",
|
||||
"verbose_name": "Answers to questions"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
|
||||
Running an export
|
||||
-----------------
|
||||
|
||||
Before you can delete data, you need to start a data export.
|
||||
Since exports often include large data sets, they might take longer than the duration of an HTTP request. Therefore,
|
||||
creating an export is a two-step process. First you need to start an export task with one of the following to API
|
||||
endpoints:
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/shredders/export/
|
||||
|
||||
Starts an export task. If your input parameters validate correctly, a ``202 Accepted`` status code is returned.
|
||||
The body points you to the download URL of the result as well as the URL for the next step.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/shredders/export/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"shredders": ["question_answers"]
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 202 Accepted
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"download": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/shredders/download/29891ede-196f-4942-9e26-d055a36e98b8/3f279f13-c198-4137-b49b-9b360ce9fcce/",
|
||||
"shred": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/shredders/shred/29891ede-196f-4942-9e26-d055a36e98b8/3f279f13-c198-4137-b49b-9b360ce9fcce/"
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:param identifier: The ``identifier`` field of the exporter to run
|
||||
:statuscode 202: no error
|
||||
:statuscode 400: Invalid input options or event data is not ready to be deleted
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
|
||||
|
||||
Downloading the result
|
||||
----------------------
|
||||
|
||||
When starting an export, you receive a ``download`` URL for downloading the result. Running a ``GET`` request on that result will
|
||||
yield one of the following status codes:
|
||||
|
||||
* ``200 OK`` – The export succeeded. The body will be your resulting file. Might be large!
|
||||
* ``409 Conflict`` – Your export is still running. The body will be JSON with the structure ``{"status": "running"}``. ``status`` can be ``waiting`` before the task is actually being processed. Please retry, but wait at least one second before you do.
|
||||
* ``410 Gone`` – Running the export has failed permanently. The body will be JSON with the structure ``{"status": "failed", "message": "Error message"}``
|
||||
* ``404 Not Found`` – The export does not exist / is expired / belongs to a different API key.
|
||||
|
||||
|
||||
Shredding the data
|
||||
------------------
|
||||
|
||||
When starting an export, you receive a ``shred`` URL for actually shredding the data.
|
||||
You can only start the actual shredding process after the export file was generated, however you are not forced to download
|
||||
the file (we'd recommend it in most cases, though).
|
||||
The download will no longer be possible after the shredding.
|
||||
Since shredding often requires deleting large data sets, it might take longer than the duration of an HTTP request.
|
||||
Therefore, shredding again is a two-step process. First you need to start a shredder task with one of the following to API
|
||||
endpoints:
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/shredders/shred/(id1)/(id2)/
|
||||
|
||||
Starts an export task. If your input parameters validate correctly, a ``202 Accepted`` status code is returned.
|
||||
The body points you to an URL you can use to check the status.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/shredders/shred/29891ede-196f-4942-9e26-d055a36e98b8/3f279f13-c198-4137-b49b-9b360ce9fcce/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 202 Accepted
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"status": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/shredders/status/29891ede-196f-4942-9e26-d055a36e98b8/3f279f13-c198-4137-b49b-9b360ce9fcce/"
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:param id1: Opaque value given to you in the previous response
|
||||
:param id2: Opaque value given to you in the previous response
|
||||
:statuscode 202: no error
|
||||
:statuscode 400: Invalid input options
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 404: The export does not exist / is expired / belongs to a different API key.
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
:statuscode 409: Your export is still running. The body will be JSON with the structure ``{"status": "running"}``. ``status`` can be ``waiting`` before the task is actually being processed. Please retry, but wait at least one second before you do.
|
||||
:statuscode 410: Either the job has timed out or running the export has failed permanently. The body will be JSON with the structure ``{"status": "failed", "message": "Error message"}``
|
||||
|
||||
|
||||
Checking the result
|
||||
-------------------
|
||||
|
||||
When starting to shred, you receive a ``status`` URL for checking for success.
|
||||
Running a ``GET`` request on that result will yield one of the following status codes:
|
||||
|
||||
* ``200 OK`` – The shredding succeeded.
|
||||
* ``409 Conflict`` – Shredding is still running. The body will be JSON with the structure ``{"status": "running"}``. ``status`` can be ``waiting`` before the task is actually being processed. Please retry, but wait at least one second before you do.
|
||||
* ``410 Gone`` – We no longer know about this process, probably the process was started more than an hour ago. Might also occur after successful operations on small pretix installations without asynchronous task handling.
|
||||
* ``417 Expectation Failed`` – Running the export has failed permanently. The body will be JSON with the structure ``{"status": "failed", "message": "Error message"}``
|
||||
@@ -59,29 +59,13 @@ seat_category_mapping object An object mappi
|
||||
last_modified datetime Last modification of this object
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 3.3
|
||||
.. versionchanged:: 4.15
|
||||
|
||||
The attributes ``geo_lat`` and ``geo_lon`` have been added.
|
||||
|
||||
.. versionchanged:: 3.10
|
||||
|
||||
The ``disabled`` attribute has been added to ``item_price_overrides`` and ``variation_price_overrides``.
|
||||
|
||||
.. versionchanged:: 3.12
|
||||
|
||||
The ``last_modified`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 3.18
|
||||
|
||||
The ``available_from``/``available_until`` attributes have been added to ``item_price_overrides`` and ``variation_price_overrides``.
|
||||
The ``search`` query parameter has been added to filter sub-events by their name or location in any language.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. versionchanged:: 3.3
|
||||
|
||||
The sub-events resource can now be filtered by meta data attributes.
|
||||
|
||||
.. versionchanged:: 4.1
|
||||
|
||||
The ``with_availability_for`` parameter has been added.
|
||||
@@ -147,6 +131,7 @@ Endpoints
|
||||
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned.
|
||||
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned.
|
||||
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned.
|
||||
:query search: Only return events matching a given search query.
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:param event: The ``slug`` field of the main event
|
||||
:query datetime modified_since: Only return objects that have changed since the given date. Be careful: This does not
|
||||
@@ -474,6 +459,7 @@ Endpoints
|
||||
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned.
|
||||
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned.
|
||||
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned.
|
||||
:query sales_channel: If set to a sales channel identifier, the response will only contain subevents from events available on this sales channel.
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:statuscode 200: no error
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
.. _`rest-vouchers`:
|
||||
|
||||
Vouchers
|
||||
========
|
||||
|
||||
@@ -17,6 +19,8 @@ max_usages integer The maximum num
|
||||
redeemed (default: 1).
|
||||
redeemed integer The number of times this voucher already has been
|
||||
redeemed.
|
||||
min_usages integer The minimum number of times this voucher must be
|
||||
redeemed on first usage (default: 1).
|
||||
valid_until datetime The voucher expiration date (or ``null``).
|
||||
block_quota boolean If ``true``, quota is blocked for this voucher.
|
||||
allow_ignore_quota boolean If ``true``, this voucher can be redeemed even if a
|
||||
@@ -46,10 +50,6 @@ show_hidden_items boolean Only if set to
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
.. versionchanged:: 3.4
|
||||
|
||||
The attribute ``seat`` has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
|
||||
@@ -30,12 +30,6 @@ subevent integer ID of the date
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
.. versionchanged:: 1.15
|
||||
|
||||
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added as well as a method to send out
|
||||
vouchers.
|
||||
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
|
||||
@@ -36,10 +36,16 @@ The following values for ``action_types`` are valid with pretix core:
|
||||
* ``pretix.event.order.canceled``
|
||||
* ``pretix.event.order.reactivated``
|
||||
* ``pretix.event.order.expired``
|
||||
* ``pretix.event.order.expirychanged``
|
||||
* ``pretix.event.order.modified``
|
||||
* ``pretix.event.order.contact.changed``
|
||||
* ``pretix.event.order.changed.*``
|
||||
* ``pretix.event.order.refund.created``
|
||||
* ``pretix.event.order.refund.created.externally``
|
||||
* ``pretix.event.order.refund.requested``
|
||||
* ``pretix.event.order.refund.done``
|
||||
* ``pretix.event.order.refund.canceled``
|
||||
* ``pretix.event.order.refund.failed``
|
||||
* ``pretix.event.order.approved``
|
||||
* ``pretix.event.order.denied``
|
||||
* ``pretix.event.checkin``
|
||||
@@ -50,6 +56,10 @@ The following values for ``action_types`` are valid with pretix core:
|
||||
* ``pretix.subevent.added``
|
||||
* ``pretix.subevent.changed``
|
||||
* ``pretix.subevent.deleted``
|
||||
* ``pretix.event.live.activated``
|
||||
* ``pretix.event.live.deactivated``
|
||||
* ``pretix.event.testmode.activated``
|
||||
* ``pretix.event.testmode.deactivated``
|
||||
|
||||
Installed plugins might register more valid values.
|
||||
|
||||
|
||||
@@ -92,9 +92,10 @@ If any other status code is returned, we will assume you did not receive the cal
|
||||
or ``304 Not Modified`` response will be treated as a failure. pretix will not follow any ``301`` or ``302`` redirect
|
||||
headers and pretix will ignore all other information in your response headers or body.
|
||||
|
||||
If we do not receive a status code in the range of ``200`` and ``299``, pretix will retry to deliver for up to three
|
||||
days with an exponential back off. Therefore, we recommend that you implement your endpoint in a way where calling it
|
||||
multiple times for the same event due to a perceived error does not do any harm.
|
||||
If we do not receive a status code in the range of ``200`` and ``299`` or do not receive any response within a 30 second
|
||||
time frame, pretix will retry to deliver for up to three days with an exponential back off. Therefore, we recommend that
|
||||
you implement your endpoint in a way where calling it multiple times for the same event due to a perceived error does
|
||||
not do any harm.
|
||||
|
||||
There is only one exception: If status code ``410 Gone`` is returned, we will assume the
|
||||
endpoint does not exist any more and automatically disable the webhook.
|
||||
|
||||
@@ -2,12 +2,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
|
||||
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
|
||||
|
||||
pricing
|
||||
checkin
|
||||
layouts
|
||||
|
||||
180
doc/development/algorithms/pricing.rst
Normal file
@@ -0,0 +1,180 @@
|
||||
.. _`algorithms-pricing`:
|
||||
|
||||
Pricing algorithms
|
||||
==================
|
||||
|
||||
With pretix being an e-commerce application, one of its core tasks is to determine the price of a purchase. With the
|
||||
complexity allowed by our range of features, this is not a trivial task and there are many edge cases that need to be
|
||||
clearly defined. The most challenging part about this is that there are many situations in which a price might change
|
||||
while the user is going through the checkout process and we're learning more information about them or their purchase.
|
||||
For example, prices change when
|
||||
|
||||
* The cart expires and the listed prices changed in the meantime
|
||||
* The user adds an invoice address that triggers a change in taxation
|
||||
* The user chooses a custom price for an add-on product and adjusts the price later on
|
||||
* The user adds a voucher to their cart
|
||||
* An automatic discount is applied
|
||||
|
||||
For the purposes of this page, we're making a distinction between "naive prices" (which are just a plain number like 23.00), and
|
||||
"taxed prices" (which are a combination of a net price, a tax rate, and a gross price, like 19.33 + 19% = 23.00).
|
||||
|
||||
Computation of listed prices
|
||||
----------------------------
|
||||
|
||||
When showing a list of products, e.g. on the event front page, we always need to show a price. This price is what we
|
||||
call the "listed price" later on.
|
||||
|
||||
To compute the listed price, we first use the ``default_price`` attribute of the ``Item`` that is being shown.
|
||||
If we are showing an ``ItemVariation`` and that variation has a ``default_price`` set on itself, the variation's price
|
||||
takes precedence and replaces the item's price.
|
||||
If we're in an event series and there exists a ``SubEventItem`` or ``SubEventItemVariation`` with a price set, the
|
||||
subevent's price configuration takes precedence over both the item as well as the variation and replaces the listed price.
|
||||
|
||||
Listed prices are naive prices. Before we actually show them to the user, we need to check if ``TaxRule.price_includes_tax``
|
||||
is set to determine if we need to add tax or subtract tax to get to the taxed price. We then consider the event's
|
||||
``display_net_prices`` setting to figure out which way to present the taxed price in the interface.
|
||||
|
||||
Guarantees on listed prices
|
||||
---------------------------
|
||||
|
||||
One goal of all further logic is that if a user sees a listed price, they are guaranteed to get the product at that
|
||||
price as long as they complete their purchase within the cart expiration time frame. For example, if the cart expiration
|
||||
time is set to 30 minutes and someone puts a item listed at €23 in their cart at 4pm, they can still complete checkout
|
||||
at €23 until 4.30pm, even if the organizer decides to raise the price to €25 at 4.10pm. If they complete checkout after
|
||||
4.30pm, their cart will be adjusted to the new price and the user will see a warning that the price has changed.
|
||||
|
||||
Computation of cart prices
|
||||
--------------------------
|
||||
|
||||
Input
|
||||
"""""
|
||||
|
||||
To ensure the guarantee mentioned above, even in the light of all possible dynamic changes, the ``listed_price``
|
||||
is explicitly stored in the ``CartPosition`` model after the item has been added to the cart.
|
||||
|
||||
If ``Item.free_price`` is set, the user is allowed to voluntarily increase the price. In this case, the user's input
|
||||
is stored as ``custom_price_input`` without much further validation for use further down below in the process.
|
||||
If ``display_net_prices`` is set, the user's input is also considered to be a net price and ``custom_price_input_is_net``
|
||||
is stored for the cart position. In any other case, the user's input is considered to be a gross price based on the tax
|
||||
rules' default tax rate.
|
||||
|
||||
The computation of prices in the cart always starts from the ``listed_price``. The ``list_price`` is only computed
|
||||
when adding the product to the cart or when extending the cart's lifetime after it expired. All other steps such as
|
||||
creating an order based on the cart trust ``list_price`` without further checks.
|
||||
|
||||
Vouchers
|
||||
""""""""
|
||||
|
||||
As a first step, the cart is checked for any voucher that should be applied to the position. If such a voucher exists,
|
||||
it's discount (percentage or fixed) is applied to the listed price. The result of this is stored to ``price_after_voucher``.
|
||||
Since ``listed_price`` naive, ``price_after_voucher`` is naive as well. As a consequence, if you have a voucher configured
|
||||
to "set the price to €10", it depends on ``TaxRule.price_includes_tax`` again whether this is €10 including or excluding
|
||||
taxes.
|
||||
|
||||
The ``price_after_voucher`` is only computed when adding the product to the cart or when extending the cart's
|
||||
lifetime after it expired. It is also checked again when the order is created, since the available discount might have
|
||||
changed due to the voucher's budget being (almost) exhausted.
|
||||
|
||||
Line price
|
||||
""""""""""
|
||||
|
||||
The next step computes the final price of this position if it is the only position in the cart. This happens in "reverse
|
||||
order", i.e. before the computation can be performed for a cart position, the step needs to be performed on all of its
|
||||
bundled positions. The sum of ``price_after_voucher`` of all bundled positions is now called ``bundled_sum``.
|
||||
|
||||
First, the value from ``price_after_voucher`` will be processed by the applicable ``TaxRule.tax()`` (which is complex
|
||||
in itself but is not documented here in detail at the moment).
|
||||
|
||||
If ``custom_price_input`` is not set, ``bundled_sum`` will be subtracted from the gross price and the net price is
|
||||
adjusted accordingly. The result is stored as ``tax_rate`` and ``line_price_gross`` in the cart position.
|
||||
|
||||
If ``custom_price_input`` is set, the value will be compared to either the gross or the net value of the ``tax()``
|
||||
result, depending on ``custom_price_input_is_net``. If the comparison yields that the custom price is higher, ``tax()``
|
||||
will be called again . Then, ``bundled_sum`` will be subtracted from the gross price and the result is stored like
|
||||
above.
|
||||
|
||||
The computation of ``line_price_gross`` from ``price_after_voucher``, ``custom_price_input``, and tax settings
|
||||
is repeated after every change of anything in the cart or after every change of the invoice address.
|
||||
|
||||
Discounts
|
||||
---------
|
||||
|
||||
After ``line_price_gross`` has been computed for all positions, the discount engine will run to apply any automatic
|
||||
discounts. Organizers can add rules for automatic discounts in the pretix backend. These rules are ordered and
|
||||
will be applied in order. Every cart position can only be "used" by one discount rule. "Used" can either mean that
|
||||
the price of the position was actually discounted, but it can also mean that the position was required to enable
|
||||
a discount for a different position, e.g. in case of a "buy 3 for the price of 2" offer.
|
||||
|
||||
The algorithm for applying an individual discount rule first starts with eliminating all products that do not match
|
||||
the rule based on its product scope. Then, the algorithm is handled differently for different configurations.
|
||||
|
||||
Case 1: Discount based on minimum value without respect to subevents
|
||||
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
* Check whether the gross sum of all positions is at least ``condition_min_value``, otherwise abort.
|
||||
|
||||
* Reduce the price of all positions by ``benefit_discount_matching_percent``.
|
||||
|
||||
* Mark all positions as "used" to hide them from further rules
|
||||
|
||||
Case 2: Discount based on minimum number of tickets without respect to subevents
|
||||
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
* Check whether the number of all positions is at least ``condition_min_count``, otherwise abort.
|
||||
|
||||
* If ``benefit_only_apply_to_cheapest_n_maches`` is set,
|
||||
|
||||
* Sort all positions by price.
|
||||
* Reduce the price of the first ``n_positions // condition_min_count * benefit_only_apply_to_cheapest_n_matches`` positions by ``benefit_discount_matching_percent``.
|
||||
* Mark the first ``n_positions // condition_min_count * condition_min_count`` as "used" to hide them from further rules.
|
||||
* Mark all positions as "used" to hide them from further rules.
|
||||
|
||||
* Else,
|
||||
|
||||
* Reduce the price of all positions by ``benefit_discount_matching_percent``.
|
||||
* Mark all positions as "used" to hide them from further rules.
|
||||
|
||||
Case 3: Discount only for products of the same subevent
|
||||
"""""""""""""""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
* Split the cart into groups based on the subevent.
|
||||
|
||||
* Proceed with case 1 or 2 for every group.
|
||||
|
||||
Case 4: Discount only for products of distinct subevents
|
||||
""""""""""""""""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
* Let ``subevents`` be a list of distinct subevents in the cart.
|
||||
|
||||
* Let ``positions[subevent]`` be a list of positions for every subevent.
|
||||
|
||||
* Let ``current_group`` be the current group and ``groups`` the list of all groups.
|
||||
|
||||
* Repeat
|
||||
|
||||
* Order ``subevents`` by the length of their ``positions[subevent]`` list, starting with the longest list.
|
||||
Do not count positions that are part of ``current_group`` already.
|
||||
|
||||
* Let ``candidates`` be the concatenation of all ``positions[subevent]`` lists with the same length as the
|
||||
longest list.
|
||||
|
||||
* If ``candidates`` is empty, abort the repetition.
|
||||
|
||||
* Order ``candidates`` by their price, starting with the lowest price.
|
||||
|
||||
* Pick one entry from ``candidates`` and put it into ``current_group``. If ``current_group`` is shorter than
|
||||
``benefit_only_apply_to_cheapest_n_matches``, we pick from the start (lowest price), otherwise we pick from
|
||||
the end (highest price)
|
||||
|
||||
* If ``current_group`` is now ``condition_min_count``, remove all entries from ``current_group`` from
|
||||
``positions[…]``, add ``current_group`` to ``groups``, and reset ``current_group`` to an empty group.
|
||||
|
||||
* For every position still left in a ``positions[…]`` list, try if there is any ``group`` in groups that it can
|
||||
still be added to without violating the rule of distinct subevents
|
||||
|
||||
* For every group in ``groups``, proceed with case 1 or 2.
|
||||
|
||||
Flowchart
|
||||
---------
|
||||
|
||||
.. image:: /images/cart_pricing.png
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
})
|
||||
@@ -60,7 +60,13 @@ The exporter class
|
||||
.. py:attribute:: BaseExporter.event
|
||||
|
||||
The default constructor sets this property to the event we are currently
|
||||
working for.
|
||||
working for. This will be ``None`` if the exporter is run for multiple
|
||||
events.
|
||||
|
||||
.. py:attribute:: BaseExporter.events
|
||||
|
||||
The default constructor sets this property to the list of events to work
|
||||
on, regardless of whether the exporter is called for one or multiple events.
|
||||
|
||||
.. autoattribute:: identifier
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -124,6 +126,8 @@ The provider class
|
||||
|
||||
.. automethod:: api_payment_details
|
||||
|
||||
.. automethod:: api_refund_details
|
||||
|
||||
.. automethod:: matching_id
|
||||
|
||||
.. automethod:: shred_payment_info
|
||||
@@ -134,6 +138,10 @@ The provider class
|
||||
|
||||
.. autoattribute:: is_meta
|
||||
|
||||
.. autoattribute:: execute_payment_needs_user
|
||||
|
||||
.. autoattribute:: multi_use_supported
|
||||
|
||||
.. autoattribute:: test_mode_message
|
||||
|
||||
.. autoattribute:: requires_invoice_immediately
|
||||
|
||||
@@ -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
|
||||
-------------------
|
||||
|
||||
@@ -184,11 +184,6 @@ Most of these methods work identically on :class:`pretix.base.models.TeamAPIToke
|
||||
Staff sessions
|
||||
--------------
|
||||
|
||||
.. versionchanged:: 1.14
|
||||
|
||||
In 1.14, the ``User.is_superuser`` attribute has been deprecated and statically set to return ``False``. Staff
|
||||
sessions have been newly introduced.
|
||||
|
||||
System administrators of a pretix instance are identified by the ``is_staff`` attribute on the user model. By default,
|
||||
the regular permission rules apply for users with ``is_staff = True``. The only difference is that such users can
|
||||
temporarily turn on "staff mode" via a button in the user interface that grants them **all permissions** as long as
|
||||
|
||||
BIN
doc/images/cart_pricing.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
28
doc/images/cart_pricing.puml
Normal file
@@ -0,0 +1,28 @@
|
||||
@startuml
|
||||
|
||||
partition "For every cart position" {
|
||||
(*) --> "Get default price from product"
|
||||
--> if "Product has variations?" then
|
||||
-->[yes] "Override with price from variation"
|
||||
--> if "Event series?" then
|
||||
-->[yes] "Override with price from subevent"
|
||||
-down-> "Store as listed_price"
|
||||
else
|
||||
-down->[no] "Store as listed_price"
|
||||
endif
|
||||
else
|
||||
-down->[no] "Store as listed_price"
|
||||
endif
|
||||
--> if "Voucher applied?" then
|
||||
-->[yes] "Apply voucher pricing"
|
||||
--> "Store as price_after_voucher"
|
||||
else
|
||||
-->[no] "Store as price_after_voucher"
|
||||
endif
|
||||
--> "Apply custom price if product allows\nApply tax rule\nSubtract bundled products"
|
||||
--> "Store as line_price (gross), tax_rate"
|
||||
}
|
||||
--> "Apply discount engine"
|
||||
--> "Store as price (gross)"
|
||||
|
||||
@enduml
|
||||
|
Before Width: | Height: | Size: 236 KiB After Width: | Height: | Size: 274 KiB |
@@ -2,8 +2,25 @@
|
||||
|
||||
|
||||
partition "data-based check" {
|
||||
"Check based on local database" --> "Is the order in status PAID or PENDING\nand is the position not canceled?"
|
||||
"Check based on local database" -down-> "Is addon_match set to true?"
|
||||
--> if "" then
|
||||
-down->[no] "Is the order in status PAID or PENDING\nand is the position not canceled?"
|
||||
else
|
||||
-right->[yes] "Build a list that includes the position\nas well as all its add-ons"
|
||||
-down-> "Filter list for products that are part of the check-in list"
|
||||
--> if "" then
|
||||
-down->[one found] Proceed with the matching position
|
||||
--> "Is the order in status PAID or PENDING\nand is the position not canceled?"
|
||||
else
|
||||
--> if "" then
|
||||
-right->[none found] "Return error PRODUCT "
|
||||
else
|
||||
-down->[multiple found] Return error AMBIGUOUS
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
"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?"
|
||||
|
||||
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 175 KiB |
@@ -19,8 +19,25 @@ else
|
||||
endif
|
||||
|
||||
|
||||
===CHECK=== -down-> "Is the order in status PAID or PENDING\nand is the position not canceled?"
|
||||
===CHECK=== -down-> "Is addon_match set to true?"
|
||||
--> if "" then
|
||||
-down->[no] "Is the order in status PAID or PENDING\nand is the position not canceled?"
|
||||
else
|
||||
-right->[yes] "Build a list that includes the position\nas well as all its add-ons"
|
||||
-down-> "Filter list for products that are part of the check-in list"
|
||||
--> if "" then
|
||||
-down->[one found] Proceed with the matching position
|
||||
--> "Is the order in status PAID or PENDING\nand is the position not canceled?"
|
||||
else
|
||||
--> if "" then
|
||||
-right->[none found] "Return error PRODUCT "
|
||||
else
|
||||
-down->[multiple found] Return error AMBIGUOUS
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
"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?"
|
||||
|
||||
@@ -52,6 +52,7 @@ Variable Description
|
||||
``order_email`` E-mail address of the ticket purchaser
|
||||
``product_id`` Internal ID of the purchased product
|
||||
``product_variation`` Internal ID of the purchased product variation (or empty)
|
||||
``secret`` The secret ticket code, would be used as the QR code for physical tickets
|
||||
``attendee_name`` Full name of the ticket holder (or empty)
|
||||
``attendee_name_*`` Name parts of the ticket holder, depending on configuration, e.g. ``attendee_name_given_name`` or ``attendee_name_family_name``
|
||||
``attendee_email`` E-mail address of the ticket holder (or empty)
|
||||
@@ -61,7 +62,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)
|
||||
@@ -90,8 +91,10 @@ Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal content ID
|
||||
title multi-lingual string The content title (required)
|
||||
internal_name string An optional name that is only used in the backend
|
||||
content_type string The type of content, valid values are ``webinar``, ``video``, ``livestream``, ``link``, ``file``
|
||||
url string The location of the digital content
|
||||
file file A downloadable file. Either ``url`` or ``file`` must be ``null``.
|
||||
description multi-lingual string A public description of the item. May contain Markdown
|
||||
syntax and is not required.
|
||||
available_from datetime The first date time at which this content will be shown
|
||||
@@ -143,6 +146,7 @@ API Endpoints
|
||||
},
|
||||
"content_type": "link",
|
||||
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
"file": null,
|
||||
"description": {
|
||||
"en": "Watch our event live here on YouTube!"
|
||||
},
|
||||
@@ -190,6 +194,7 @@ API Endpoints
|
||||
},
|
||||
"content_type": "link",
|
||||
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
"file": null,
|
||||
"description": {
|
||||
"en": "Watch our event live here on YouTube!"
|
||||
},
|
||||
@@ -228,6 +233,7 @@ API Endpoints
|
||||
},
|
||||
"content_type": "link",
|
||||
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
"file": null,
|
||||
"description": {
|
||||
"en": "Watch our event live here on YouTube!"
|
||||
},
|
||||
@@ -254,6 +260,7 @@ API Endpoints
|
||||
},
|
||||
"content_type": "link",
|
||||
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
"file": null,
|
||||
"description": {
|
||||
"en": "Watch our event live here on YouTube!"
|
||||
},
|
||||
@@ -308,6 +315,7 @@ API Endpoints
|
||||
},
|
||||
"content_type": "link",
|
||||
"url": "https://mywebsite.com",
|
||||
"file": null,
|
||||
"description": {
|
||||
"en": "Watch our event live here on YouTube!"
|
||||
},
|
||||
|
||||
734
doc/plugins/exhibitors.rst
Normal file
@@ -0,0 +1,734 @@
|
||||
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``, ``value``, and ``details``. ``details`` is usually empty
|
||||
except in a few cases where it contains an additional list of objects
|
||||
with ``value`` and ``label`` keys (e.g. splitting of names).
|
||||
device_name string User-defined name for the device used for scanning (or ``null``).
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
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 Miller",
|
||||
"details": [
|
||||
{"label": "Given name", "value": "Peter"},
|
||||
{"label": "Family name", "value": "Miller"},
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
: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:get:: /api/v1/organizers/(organizer)/events/(event)/exhibitors/(id)/vouchers/
|
||||
|
||||
Returns a list of all vouchers connected to an exhibitor. The response contains the same data as described in
|
||||
:ref:`rest-vouchers`.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/exhibitors/1/vouchers/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"code": "43K6LKM37FBVR2YG",
|
||||
"max_usages": 1,
|
||||
"redeemed": 0,
|
||||
"valid_until": null,
|
||||
"block_quota": false,
|
||||
"allow_ignore_quota": false,
|
||||
"price_mode": "set",
|
||||
"value": "12.00",
|
||||
"item": 1,
|
||||
"variation": null,
|
||||
"quota": null,
|
||||
"tag": "testvoucher",
|
||||
"comment": "",
|
||||
"seat": 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
|
||||
: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/(id)/vouchers/attach/
|
||||
|
||||
Attaches an **existing** voucher to an exhibitor. You need to send either the ``id`` **or** the ``code`` field of
|
||||
the voucher.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/exhibitors/1/vouchers/attach/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"id": 15
|
||||
}
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/exhibitors/1/vouchers/attach/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"code": "43K6LKM37FBVR2YG"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{}
|
||||
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:param event: The ``slug`` field of the event to use
|
||||
:param id: The ``id`` field of the exhibitor to use
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: Invalid data sent, e.g. voucher does not exist
|
||||
: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.
|
||||
├ terms_url string URL to terms of service. 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,
|
||||
"terms_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",
|
||||
"details": [
|
||||
{"label": "Given name", "value": "John"},
|
||||
{"label": "Family name", "value": "Doe"},
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "attendee_email",
|
||||
"label": "Email",
|
||||
"value": "test@example.com",
|
||||
"details": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"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
|
||||
|
||||
You can also fetch existing leads (if you are authorized to do so):
|
||||
|
||||
.. http:get:: /exhibitors/api/v1/leads/
|
||||
|
||||
**Example request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /exhibitors/api/v1/leads/ 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
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"attendee": {
|
||||
"fields": [
|
||||
{
|
||||
"id": "attendee_name",
|
||||
"label": "Name",
|
||||
"value": "Jon Doe",
|
||||
"details": [
|
||||
{"label": "Given name", "value": "John"},
|
||||
{"label": "Family name", "value": "Doe"},
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "attendee_email",
|
||||
"label": "Email",
|
||||
"value": "test@example.com",
|
||||
"details": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"rating": 4,
|
||||
"tags": ["foo"],
|
||||
"notes": "Great customer, wants our newsletter"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:statuscode 200: No error
|
||||
:statuscode 401: Invalid authentication code
|
||||
:statuscode 403: Not permitted to access bulk data
|
||||
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,11 +12,14 @@ If you want to **create** a plugin, please go to the
|
||||
:maxdepth: 2
|
||||
|
||||
list
|
||||
pretixdroid
|
||||
banktransfer
|
||||
ticketoutputpdf
|
||||
badges
|
||||
campaigns
|
||||
certificates
|
||||
digital
|
||||
exhibitors
|
||||
shipping
|
||||
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
@@ -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
|
||||
235
doc/plugins/shipping.rst
Normal file
@@ -0,0 +1,235 @@
|
||||
Shipping
|
||||
========
|
||||
|
||||
The shipping plugin provides a HTTP API that exposes the various layouts used to generate PDF badges.
|
||||
|
||||
Shipping address resource
|
||||
-------------------------
|
||||
|
||||
The shipping address resource contains the following public fields:
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
===================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
company string Customer company name
|
||||
name string Customer name
|
||||
street string Customer street
|
||||
zipcode string Customer ZIP code
|
||||
city string Customer city
|
||||
country string Customer country code
|
||||
state string Customer state (ISO 3166-2 code). Only supported in
|
||||
AU, BR, CA, CN, MY, MX, and US.
|
||||
gift boolean Request by customer to not disclose prices in the shipping
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
Shipping status resource
|
||||
------------------------
|
||||
|
||||
The shipping status resource contains the following public fields:
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
===================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
method integer Internal ID of shipping method
|
||||
status string Status, one of ``"new"`` or ``"shipped"``
|
||||
method_type string Method type, one of ``"ship"``, ``"online"``, or ``"collect"``
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
Print job resource
|
||||
------------------
|
||||
|
||||
The print job resource contains the following public fields:
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
===================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
code string Order code of the ticket order
|
||||
event string Event slug
|
||||
status string Status, one of ``"new"`` or ``"shipped"``
|
||||
method string Method type, one of ``"ship"``, ``"online"``, or ``"collect"``
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/shippingaddress/
|
||||
|
||||
Returns the shipping address of an order
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/democon/orders/ABC12/shippingaddress/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"company": "ACME Corp",
|
||||
"name": "John Doe",
|
||||
"street": "Sesame Street 12\nAp. 5",
|
||||
"zipcode": "12345",
|
||||
"city": "Berlin",
|
||||
"country": "DE",
|
||||
"state": "",
|
||||
"gift": false
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:param event: The ``slug`` field of a valid event
|
||||
:param order: The ``code`` field of a valid order
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
||||
:statuscode 404: The order does not exist or no shipping address is attached.
|
||||
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/shippingaddress/
|
||||
|
||||
Returns the shipping status of an order
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/democon/orders/ABC12/shippingstatus/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"method": 23,
|
||||
"method_type": "ship",
|
||||
"status": "new"
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:param event: The ``slug`` field of a valid event
|
||||
:param order: The ``code`` field of a valid order
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
||||
:statuscode 404: The order does not exist or no shipping address is attached.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/printjobs/
|
||||
|
||||
Returns a list of ticket orders, only useful with some query filters
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/printjobs/?method=ship&status=new HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"event": "democon",
|
||||
"order": "ABC12",
|
||||
"method": "ship",
|
||||
"status": "new"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query string method: Filter by response field ``method`` (can be passed multiple times)
|
||||
:query string status: Filter by response field ``status``
|
||||
:query string event: Filter by response field ``event``
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:param event: The ``slug`` field of a valid event
|
||||
: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)/printjobs/poll/
|
||||
|
||||
Returns the PDF file for the next job to print.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/printjobs/poll/?method=ship&status=new 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
|
||||
X-Pretix-Order-Code: ABC12
|
||||
|
||||
...
|
||||
|
||||
:query string method: Filter by response field ``method`` (can be passed multiple times)
|
||||
:query string status: Filter by response field ``status``
|
||||
:query string event: Filter by response field ``event``
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:param event: The ``slug`` field of a valid event
|
||||
: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:post:: /api/v1/organizers/(organizer)/printjobs/(order)/ack/
|
||||
|
||||
Change an order's status to "shipped".
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/printjobs/ABC12/ack/ 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 a valid organizer
|
||||
:param event: The ``slug`` field of a valid event
|
||||
:param order: The ``code`` field of a valid order
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
||||
:statuscode 404: The order does not exist.
|
||||
10
doc/requirements.rtd.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
sphinx==2.3.*
|
||||
jinja2==3.0.*
|
||||
sphinx-rtd-theme
|
||||
sphinxcontrib-httpdomain
|
||||
sphinxcontrib-images
|
||||
sphinxcontrib-spelling==4.*
|
||||
sphinxemoji
|
||||
pygments-markdown-lexer
|
||||
# See https://github.com/rfk/pyenchant/pull/130
|
||||
git+https://github.com/raphaelm/pyenchant.git@patch-1#egg=pyenchant
|
||||
@@ -1,5 +1,6 @@
|
||||
-e ../src/
|
||||
sphinx==2.3.*
|
||||
jinja2==3.0.*
|
||||
sphinx-rtd-theme
|
||||
sphinxcontrib-httpdomain
|
||||
sphinxcontrib-images
|
||||
|
||||
BIN
doc/screens/organizer/customer.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
doc/screens/organizer/customer_edit.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
doc/screens/organizer/customer_ssoclient_add.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
doc/screens/organizer/customer_ssoprovider_add.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
doc/screens/organizer/customers.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
doc/screens/organizer/edit_customer.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
@@ -66,6 +66,7 @@ iterable
|
||||
Jimdo
|
||||
jwt
|
||||
JWT
|
||||
JWTs
|
||||
libpretixprint
|
||||
libsass
|
||||
linters
|
||||
@@ -88,7 +89,9 @@ nginx
|
||||
nodejs
|
||||
NotificationType
|
||||
npm
|
||||
OIDC
|
||||
ons
|
||||
OpenID
|
||||
optimizations
|
||||
overpayment
|
||||
param
|
||||
@@ -103,6 +106,7 @@ prepending
|
||||
preprocessor
|
||||
presale
|
||||
pretix
|
||||
pretixLEAD
|
||||
pretixSCAN
|
||||
pretixdroid
|
||||
pretixPOS
|
||||
@@ -132,6 +136,7 @@ serializer
|
||||
serializers
|
||||
sexualized
|
||||
SQL
|
||||
SSO
|
||||
startup
|
||||
stdout
|
||||
stylesheet
|
||||
@@ -158,6 +163,8 @@ untrusted
|
||||
uptime
|
||||
username
|
||||
url
|
||||
URI
|
||||
URIs
|
||||
validator
|
||||
versa
|
||||
versioning
|
||||
|
||||
210
doc/user/customers/index.rst
Normal file
@@ -0,0 +1,210 @@
|
||||
.. _customers:
|
||||
|
||||
Customer accounts
|
||||
=================
|
||||
|
||||
By default, pretix only offers guest checkout, i.e. ticket buyers do not sign up and sign back in, but create a new
|
||||
checkout session every time. In some situations it may be convenient to allow ticket buyers to create
|
||||
accounts that they can later log in to again. Working with customer accounts is even required for some advanced
|
||||
use cases such as described in the :ref:`seasontickets` article.
|
||||
|
||||
Enabling customer accounts
|
||||
--------------------------
|
||||
|
||||
To enable customer accounts, head to your organizer page in the backend and then select "Settings" → "General" →
|
||||
"Customer accounts" and turn on the checkbox "Allow customers to create accounts".
|
||||
|
||||
Using the other settings on the same tab you can fine-tune how the customer account system behaves:
|
||||
|
||||
.. thumbnail:: ../../screens/organizer/edit_customer.png
|
||||
:align: center
|
||||
:class: screenshot
|
||||
|
||||
Allow customers to log in with email address and password
|
||||
In all simple setups, this option should be checked. If this checkbox is removed, it is impossible to log in or
|
||||
sign up unless you connect a SSO provider (see below).
|
||||
|
||||
Match orders based on email address
|
||||
If this option is selected, customers will see orders made with their email address within their account even if
|
||||
they did not make those orders while logged in.
|
||||
|
||||
Name format, Allowed titles
|
||||
This controls how we'll ask your customers for their name, similar to the respective settings on event level.
|
||||
|
||||
Managing customer accounts
|
||||
--------------------------
|
||||
|
||||
After customer accounts have been enabled, you will find a new menu option "Customer accounts" in the organizer-level
|
||||
main menu. The first sub-item, "Customers", allows you to search and inspect the list of your customer accounts, as well
|
||||
as to create a new customer account from the backend:
|
||||
|
||||
.. thumbnail:: ../../screens/organizer/customers.png
|
||||
:align: center
|
||||
:class: screenshot
|
||||
|
||||
If you click on a customer ID, you can see all details of this customer account, including registration information,
|
||||
active memberships, past ticket orders, and account history:
|
||||
|
||||
.. thumbnail:: ../../screens/organizer/customer.png
|
||||
:align: center
|
||||
:class: screenshot
|
||||
|
||||
You can also perform various actions from this view, such as:
|
||||
|
||||
- Send a password reset link
|
||||
- Change registration information
|
||||
- Anonymize the customer account (does not anonymize connected orders)
|
||||
|
||||
When creating or changing a customer, you will be presented with the following form:
|
||||
|
||||
.. thumbnail:: ../../screens/organizer/customer_edit.png
|
||||
:align: center
|
||||
:class: screenshot
|
||||
|
||||
Most fields, such as name, e-mail address, phone number, and language should be self-explanatory. The following fields
|
||||
might require some explanation:
|
||||
|
||||
Account active
|
||||
If this checkbox is removed, the customer will not be able to log in.
|
||||
|
||||
External identifier
|
||||
This field can be used to cross-reference your customer database with other sources. For example, if the customer
|
||||
already has a number in another system, you can insert that number here. This can be especially powerful if you
|
||||
use our API for synchronization with an external system.
|
||||
|
||||
Verified email address
|
||||
This checkbox signifies whether you have verified that this customer in fact controls the given email address.
|
||||
This will automatically be checked after a successful registration or after a successful password reset. Before it
|
||||
is checked, the customer will not be able to log in. You should usually not modify this field manually.
|
||||
|
||||
Notes
|
||||
Entries in this field will only be visible to you and your team, not to the customer.
|
||||
|
||||
Single-Sign-On (SSO)
|
||||
--------------------
|
||||
|
||||
"Single-Sign-On" (SSO) is a technical term for a situation in which a person can log in to multiple systems using just
|
||||
one login. This can be convenient if you have multiple applications that are exposed to your customers: They won't have
|
||||
to remember multiple passwords or understand how your application landscape is structured, they can just always log in
|
||||
with the same credentials whenever they see your brand.
|
||||
|
||||
In this scenario, pretix can be **either** the "SSO provider" **or** the "SSO client".
|
||||
If pretix is the SSO provider, pretix will be the central source of truth for your customer accounts and your other
|
||||
applications can connect to pretix to use pretix's login functionality.
|
||||
If pretix is the SSO client, one of your existing systems will be the source of truth for the customer accounts and
|
||||
pretix will use that system's login functionality.
|
||||
|
||||
All SSO support for customer accounts in pretix is currently built on the `OpenID Connect`_ standard, a modern and
|
||||
widely accepted standard for SSO in all industries.
|
||||
|
||||
Connecting SSO clients (pretix as the SSO provider)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
To connect an external application as a SSO client, go to "Customer accounts" → "SSO clients" → "Create a new SSO client"
|
||||
in your organizer account.
|
||||
|
||||
.. thumbnail:: ../../screens/organizer/customer_ssoclient_add.png
|
||||
:align: center
|
||||
:class: screenshot
|
||||
|
||||
You will need to fill out the following fields:
|
||||
|
||||
Active
|
||||
If this checkbox is removed, the SSO client can not be used.
|
||||
|
||||
Application name
|
||||
The name of your external application, e.g. "digital event marketplace".
|
||||
|
||||
Client type
|
||||
For a server-side application which is able to store a secret that will be inaccessible to end users, chose
|
||||
"confidential". For a client-side application, such as many mobile apps, choose "public".
|
||||
|
||||
Grant type
|
||||
This value depends on the OpenID Connect implementation of your software.
|
||||
|
||||
Redirection URIs
|
||||
One or multiple URIs that the user might be redirected to after the successful or failed login.
|
||||
|
||||
Allowed access scopes
|
||||
The types of data the SSO client may access about the customer.
|
||||
|
||||
After you submitted all data, you will receive a client ID as well as a client secret. The client secret is shown
|
||||
in the green success message and will only ever be shown once. If you need it again, use the option "Invalidate old
|
||||
client secret and generate a new one".
|
||||
|
||||
You will need the client ID and client secret to configure your external application. The application will also likely
|
||||
need some other information from you, such as your **issuer URI**. If you use pretix Hosted and your organizer account
|
||||
does not have a custom domain, your issuer will be ``https://pretix.eu/myorgname``, where ``myorgname`` is the short
|
||||
form of your organizer account. If you use a custom domain, such as ``tickets.mycompany.net``, then your issuer will be
|
||||
``https://tickets.mycompany.net``.
|
||||
|
||||
Technical details
|
||||
"""""""""""""""""
|
||||
|
||||
We implement `OpenID Connect Core 1.0`_, except for some optional parts that do not make sense for pretix or bring no
|
||||
additional value. For example, we do not currently support encrypted tokens, offline access, refresh tokens, or passing
|
||||
request parameters as JWTs.
|
||||
|
||||
We implement the provider metadata section from `OpenID Connect Discovery 1.0`_. You can find the endpoint relative
|
||||
to the issuer URI as described above, for example ``http://pretix.eu/demo/.well-known/openid-configuration``.
|
||||
|
||||
We implement all three OpenID Connect Core flows:
|
||||
|
||||
- Authorization Code Flow (response type ``code``)
|
||||
- Implicit Flow (response types ``id_token token`` and ``id_token``)
|
||||
- Hybrid Flow (response types ``code id_token``, ``code id_token token``, and ``code token``)
|
||||
|
||||
We implement the response modes ``query`` and ``fragment``.
|
||||
|
||||
We currently offer the following scopes: ``openid``, ``profile``, ``email``, ``phone``
|
||||
|
||||
As well as the following standardized claims: ``iss``, ``aud``, ``exp``, ``iat``, ``auth_time``, ``nonce``, ``c_hash``,
|
||||
``at_hash``, ``sub``, ``locale``, ``name``, ``given_name``, ``family_name``, ``middle_name``, ``nickname``, ``email``,
|
||||
``email_verified``, ``phone_number``.
|
||||
|
||||
The various endpoints are located relative to the issuer URI as described above:
|
||||
|
||||
- Authorization: ``<issuer>/oauth2/v1/authorize``
|
||||
- Token: ``<issuer>/oauth2/v1/token``
|
||||
- User info: ``<issuer>/oauth2/v1/userinfo``
|
||||
- Keys: ``<issuer>/oauth2/v1/keys``
|
||||
|
||||
We currently do not reproduce their documentation here as they follow the OpenID Connect and OAuth specifications
|
||||
without any special behavior.
|
||||
|
||||
Connecting SSO providers (pretix as the SSO client)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
To connect an external application as a SSO client, go to "Customer accounts" → "SSO providers" → "Create a new SSO provider"
|
||||
in your organizer account.
|
||||
|
||||
.. thumbnail:: ../../screens/organizer/customer_ssoprovider_add.png
|
||||
:align: center
|
||||
:class: screenshot
|
||||
|
||||
The "Provider name" and "Login button label" is what we'll use to show the new login option to the user. For the actual
|
||||
connection, we will require information such as the issuer URL, client ID, client secret, scope, and field (or claim)
|
||||
names that you will receive from your SSO provider.
|
||||
|
||||
.. note::
|
||||
|
||||
If you want your customers to *only* use your SSO provider, it makes sense to turn off the "Allow customers to log in
|
||||
with email address and password" settings option (see above).
|
||||
|
||||
Technical details
|
||||
"""""""""""""""""
|
||||
|
||||
We assume that SSO providers fulfill the following requirements:
|
||||
|
||||
- Implementation according to `OpenID Connect Core 1.0`_.
|
||||
|
||||
- Published meta-data document at ``<issuer>/.well-known/openid-configuration`` as specified in `OpenID Connect Discovery 1.0`_.
|
||||
|
||||
- Support for Authorization code flow (``response_type=code``) with ``response_mode=query``.
|
||||
|
||||
- Support for client authentication using client ID and client secret and without public key cryptography.
|
||||
|
||||
|
||||
.. _OpenID Connect: https://en.wikipedia.org/wiki/OpenID#OpenID_Connect_(OIDC)
|
||||
.. _OpenID Connect Core 1.0: https://openid.net/specs/openid-connect-core-1_0.html
|
||||
.. _OpenID Connect Discovery 1.0: https://openid.net/specs/openid-connect-discovery-1_0.html
|
||||
@@ -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,20 +1,44 @@
|
||||
Use case: Group discounts
|
||||
-------------------------
|
||||
|
||||
Often times, you want to give discounts for whole groups attending your event. pretix can't automatically discount based on volume, but there's still some ways you can set up group tickets.
|
||||
Often times, you want to give discounts for whole groups attending your event.
|
||||
|
||||
Flexible group sizes
|
||||
Automatic discounts
|
||||
"""""""""""""""""""
|
||||
|
||||
pretix can automatically grant discounts if a certain condition is met, such as a specific group size. To set this up,
|
||||
head to **Products**, **Discounts** in the event navigation and **Create a new discount**. You can choose a name so you
|
||||
can later find this again. You can also optionally restrict the discount to a specific time frame or a specific sales
|
||||
channel.
|
||||
|
||||
Next, either select **Apply to all products** or create a selection of products that are eligible for the discount.
|
||||
|
||||
For a **percentual group discount** similar to "if you buy at least 5 tickets, you get 20 percent off", set
|
||||
**Minimum number of matching products** to "5" and **Percentual discount on matching products** to "20.00".
|
||||
|
||||
For a **buy-X-get-Y discount**, e.g. "if you buy 5 tickets, you get one free", set
|
||||
**Minimum number of matching products** to "5", **Percentual discount on matching products** to "100.00", and
|
||||
**Apply discount only to this number of matching products** to "1".
|
||||
|
||||
Fixed group packages
|
||||
""""""""""""""""""""
|
||||
|
||||
If you want to give out discounted tickets to groups starting at a given size, but still billed per person, you can do so by creating a special **Group ticket** at the per-person price and set the **Minimum amount per order** option of the ticket to the minimal group size.
|
||||
If you want to sell group tickets in fixed sizes, e.g. a table of eight at your gala dinner, you can use product bundles.
|
||||
Assuming you already set up a ticket for admission of single persons, you then set up a second product **Table (8 persons)**
|
||||
with a discounted full price. Then, head to the **Bundled products** tab of that product and add one bundle configuration
|
||||
to include the single admission product **eight times**. Next, create an unlimited quota mapped to the new product.
|
||||
|
||||
This way, the purchase of a table will automatically create eight tickets, leading to a correct calculation of your total
|
||||
quota and, as expected, eight persons on your check-in list. You can even ask for the individual names of the persons
|
||||
during checkout.
|
||||
|
||||
Minimum order amount
|
||||
""""""""""""""""""""
|
||||
|
||||
If you want to promote discounted group tickets in your price list, you can also do so by creating a special
|
||||
**Group ticket** at the reduced per-person price and set the **Minimum amount per order** option of the ticket to the minimal
|
||||
group size.
|
||||
|
||||
For more complex use cases, you can also use add-on products that can be chosen multiple times.
|
||||
|
||||
This way, your ticket can be bought an arbitrary number of times – but no less than the given minimal amount per order.
|
||||
|
||||
Fixed group sizes
|
||||
"""""""""""""""""
|
||||
|
||||
If you want to sell group tickets in fixed sizes, e.g. a table of eight at your gala dinner, you can use product bundles. Assuming you already set up a ticket for admission of single persons, you then set up a second product **Table (8 persons)** with a discounted full price. Then, head to the **Bundled products** tab of that product and add one bundle configuration to include the single admission product **eight times**. Next, create an unlimited quota mapped to the new product.
|
||||
|
||||
This way, the purchase of a table will automatically create eight tickets, leading to a correct calculation of your total quota and, as expected, eight persons on your check-in list. You can even ask for the individual names of the persons during checkout.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
.. _secret_generators:
|
||||
|
||||
Ticket secret generators
|
||||
========================
|
||||
|
||||
@@ -76,7 +78,7 @@ Synchronization setting any
|
||||
----------------------------------------------- ----------------------------------- ----------------------------------------------------------------------- -----------------------------------------------------------------------
|
||||
Ticket secrets any Random Signed Random Signed
|
||||
=============================================== =================================== =================================== =================================== ================================= =====================================
|
||||
Scenario supported on platforms Android, Desktop, iOS Android, Desktop, iOS Android, Desktop Android, Desktop Android, Desktop
|
||||
Scenario supported on platforms Android, Desktop, iOS Android, Desktop, iOS Android, Desktop Android, Desktop, iOS Android, Desktop, iOS
|
||||
Synchronization speed for large data sets slow slow fast fast
|
||||
Tickets can be scanned yes yes yes no yes
|
||||
Ticket is valid after sale immediately next sync (~5 minutes) immediately never immediately
|
||||
@@ -88,6 +90,7 @@ Name and seat visible on scanner yes
|
||||
Order-specific check-in attention flag yes yes yes (except directly after sale) n/a no
|
||||
Ticket search by order code or name yes yes yes (except directly after sale) no no
|
||||
Check-in statistics on scanner yes yes mostly accurate no no
|
||||
Support for add-on check-in with main ticket yes yes yes (except directly after sale) no no
|
||||
=============================================== =================================== =================================== =================================== ================================= =====================================
|
||||
|
||||
.. _EdDSA: https://en.wikipedia.org/wiki/EdDSA#Ed25519
|
||||
|
||||
@@ -87,6 +87,18 @@ website. If you confident to have a good reason for not using SSL, you can overr
|
||||
|
||||
<pretix-widget event="https://pretix.eu/demo/democon/" skip-ssl-check></pretix-widget>
|
||||
|
||||
Seating plans
|
||||
-------------
|
||||
|
||||
By default, events with seating plans just show a button that opens the seating plan. You can also have the seating
|
||||
plan embedded into the widget directly by using::
|
||||
|
||||
<pretix-widget event="https://pretix.eu/demo/democon/" seating-embedded></pretix-widget>
|
||||
|
||||
Note that the seating plan will only be embedded if the widget has enough space (currently min. 992 pixels width, may change
|
||||
in the future) and that the seating plan part of the widget can unfortunately *not* be styled with CSS like the rest of
|
||||
the widget.
|
||||
|
||||
Always open a new tab
|
||||
---------------------
|
||||
|
||||
@@ -135,6 +147,10 @@ Alternatively, you can select one or more categories to be shown::
|
||||
|
||||
<pretix-widget event="https://pretix.eu/demo/democon/" categories="12,25"></pretix-widget>
|
||||
|
||||
Or variation IDs::
|
||||
|
||||
<pretix-widget event="https://pretix.eu/demo/democon/" variations="15,2,68"></pretix-widget>
|
||||
|
||||
Multi-event selection
|
||||
---------------------
|
||||
|
||||
@@ -253,18 +269,21 @@ If you want, you can suppress us loading the widget and/or modify the user data
|
||||
|
||||
If you then later want to trigger loading the widgets, just call ``window.PretixWidget.buildWidgets()``.
|
||||
|
||||
Waiting for the widget to load
|
||||
------------------------------
|
||||
Waiting for the widget to load or close
|
||||
---------------------------------------
|
||||
|
||||
If you want to run custom JavaScript once the widget is fully loaded, you can register a callback function. Note that
|
||||
this function might be run multiple times, for example if you have multiple widgets on a page or if the user switches
|
||||
e.g. from an event list to an event detail view::
|
||||
If you want to run custom JavaScript once the widget is fully loaded or when it is closed, you can register callback
|
||||
functions. Note that these function might be run multiple times, for example if you have multiple widgets on a page
|
||||
or if the user switches e.g. from an event list to an event detail view::
|
||||
|
||||
<script type="text/javascript">
|
||||
window.pretixWidgetCallback = function () {
|
||||
window.PretixWidget.addLoadListener(function () {
|
||||
console.log("Widget has loaded!");
|
||||
});
|
||||
window.PretixWidget.addCloseListener(function () {
|
||||
console.log("Widget has been closed!");
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -309,6 +328,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:
|
||||
|
||||
@@ -400,7 +423,7 @@ Hosted or pretix Enterprise are active, you can pass the following fields:
|
||||
};
|
||||
</script>
|
||||
|
||||
If you use ```analytics.js` (Universal Analytics)::
|
||||
If you use ``analytics.js`` (Universal Analytics)::
|
||||
|
||||
<script>
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
@@ -436,8 +459,4 @@ Hosted or pretix Enterprise are active, you can pass the following fields:
|
||||
</script>
|
||||
|
||||
|
||||
.. versionchanged:: 3.6
|
||||
|
||||
Dynamically opening the widget has been added in pretix 3.6.
|
||||
|
||||
.. _Let's Encrypt: https://letsencrypt.org/
|
||||
|
||||
@@ -84,7 +84,9 @@ going to develop around pretix, for example connect to pretix through our API, y
|
||||
- A voucher is a code that can be used for multiple purposes: To grant a discount to specific customers, to only
|
||||
show certain products to certain customers, or to keep a seat open for someone specific even though you are
|
||||
sold out. If a voucher is used to apply a discount, the price of the purchased product is reduced by the
|
||||
discounted amount. Vouchers are connected to a specific event.
|
||||
* - | |:gb:| **(Automatic) Discount**
|
||||
| |:de:| (Automatischer) Rabatt
|
||||
- Discounts can be used to automatically provide discounts to customers if their cart satisfies a certain condition.
|
||||
* - | |:gb:| **Gift card**
|
||||
| |:de:| Wertgutschein
|
||||
- A :ref:`gift card <giftcards>` is a coupon representing an exact amount of money that can be used for purchases
|
||||
|
||||
@@ -12,6 +12,7 @@ wanting to use pretix to sell tickets.
|
||||
events/settings
|
||||
events/structureguide
|
||||
events/widget
|
||||
customers/index
|
||||
events/giftcards
|
||||
faq
|
||||
markdown
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
|
||||
build:
|
||||
image: latest
|
||||
|
||||
python:
|
||||
version: 3.6
|
||||
@@ -13,6 +13,9 @@ recursive-include pretix/plugins/banktransfer/static *
|
||||
recursive-include pretix/plugins/manualpayment/templates *
|
||||
recursive-include pretix/plugins/manualpayment/static *
|
||||
recursive-include pretix/plugins/paypal/templates *
|
||||
recursive-include pretix/plugins/paypal/static *
|
||||
recursive-include pretix/plugins/paypal2/templates *
|
||||
recursive-include pretix/plugins/paypal2/static *
|
||||
recursive-include pretix/plugins/pretixdroid/templates *
|
||||
recursive-include pretix/plugins/pretixdroid/static *
|
||||
recursive-include pretix/plugins/sendmail/templates *
|
||||
|
||||
@@ -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.5.0.dev0"
|
||||
__version__ = "4.16.0.dev0"
|
||||
|
||||
@@ -45,6 +45,8 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
|
||||
allowlist = (
|
||||
('GET', 'api-v1:version'),
|
||||
('GET', 'api-v1:device.eventselection'),
|
||||
('GET', 'api-v1:idempotency.query'),
|
||||
('GET', 'api-v1:device.info'),
|
||||
('POST', 'api-v1:device.update'),
|
||||
('POST', 'api-v1:device.revoke'),
|
||||
('POST', 'api-v1:device.roll'),
|
||||
@@ -67,6 +69,8 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'api-v1:orderposition-pdf_image'),
|
||||
('GET', 'api-v1:event.settings'),
|
||||
('POST', 'api-v1:upload'),
|
||||
('POST', 'api-v1:checkinrpc.redeem'),
|
||||
('GET', 'api-v1:checkinrpc.search'),
|
||||
)
|
||||
|
||||
|
||||
@@ -76,6 +80,8 @@ class PretixScanNoSyncNoSearchSecurityProfile(AllowListSecurityProfile):
|
||||
allowlist = (
|
||||
('GET', 'api-v1:version'),
|
||||
('GET', 'api-v1:device.eventselection'),
|
||||
('GET', 'api-v1:idempotency.query'),
|
||||
('GET', 'api-v1:device.info'),
|
||||
('POST', 'api-v1:device.update'),
|
||||
('POST', 'api-v1:device.revoke'),
|
||||
('POST', 'api-v1:device.roll'),
|
||||
@@ -96,6 +102,8 @@ class PretixScanNoSyncNoSearchSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'api-v1:orderposition-pdf_image'),
|
||||
('GET', 'api-v1:event.settings'),
|
||||
('POST', 'api-v1:upload'),
|
||||
('POST', 'api-v1:checkinrpc.redeem'),
|
||||
('GET', 'api-v1:checkinrpc.search'),
|
||||
)
|
||||
|
||||
|
||||
@@ -105,6 +113,8 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
|
||||
allowlist = (
|
||||
('GET', 'api-v1:version'),
|
||||
('GET', 'api-v1:device.eventselection'),
|
||||
('GET', 'api-v1:idempotency.query'),
|
||||
('GET', 'api-v1:device.info'),
|
||||
('POST', 'api-v1:device.update'),
|
||||
('POST', 'api-v1:device.revoke'),
|
||||
('POST', 'api-v1:device.roll'),
|
||||
@@ -126,6 +136,8 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'api-v1:orderposition-pdf_image'),
|
||||
('GET', 'api-v1:event.settings'),
|
||||
('POST', 'api-v1:upload'),
|
||||
('POST', 'api-v1:checkinrpc.redeem'),
|
||||
('GET', 'api-v1:checkinrpc.search'),
|
||||
)
|
||||
|
||||
|
||||
@@ -135,6 +147,8 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
|
||||
allowlist = (
|
||||
('GET', 'api-v1:version'),
|
||||
('GET', 'api-v1:device.eventselection'),
|
||||
('GET', 'api-v1:idempotency.query'),
|
||||
('GET', 'api-v1:device.info'),
|
||||
('POST', 'api-v1:device.update'),
|
||||
('POST', 'api-v1:device.revoke'),
|
||||
('POST', 'api-v1:device.roll'),
|
||||
@@ -151,6 +165,8 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'api-v1:ticketlayoutitem-list'),
|
||||
('GET', 'api-v1:badgelayout-list'),
|
||||
('GET', 'api-v1:badgeitem-list'),
|
||||
('GET', 'api-v1:voucher-list'),
|
||||
('GET', 'api-v1:voucher-detail'),
|
||||
('GET', 'api-v1:order-list'),
|
||||
('POST', 'api-v1:order-list'),
|
||||
('GET', 'api-v1:order-detail'),
|
||||
@@ -167,6 +183,8 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
|
||||
('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'),
|
||||
@@ -174,8 +192,12 @@ 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'),
|
||||
('POST', 'plugins:pretix_posbackend:stripeterminal.paymentintent'),
|
||||
('PUT', 'plugins:pretix_posbackend:file.upload'),
|
||||
('GET', 'api-v1:revokedsecrets-list'),
|
||||
('GET', 'api-v1:event.settings'),
|
||||
('GET', 'plugins:pretix_seating:event.event'),
|
||||
@@ -183,6 +205,8 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'plugins:pretix_seating:event.plan'),
|
||||
('GET', 'plugins:pretix_seating:selection.simple'),
|
||||
('POST', 'api-v1:upload'),
|
||||
('POST', 'api-v1:checkinrpc.redeem'),
|
||||
('GET', 'api-v1:checkinrpc.search'),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -19,11 +19,17 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import logging
|
||||
|
||||
import ujson
|
||||
from rest_framework import exceptions
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import exception_handler, status
|
||||
|
||||
from pretix.base.services.locking import LockTimeoutException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def custom_exception_handler(exc, context):
|
||||
response = exception_handler(exc, context)
|
||||
@@ -37,4 +43,7 @@ def custom_exception_handler(exc, context):
|
||||
}
|
||||
)
|
||||
|
||||
if isinstance(exc, exceptions.APIException):
|
||||
logger.info(f'API Exception [{exc.status_code}]: {ujson.dumps(exc.detail)}')
|
||||
|
||||
return response
|
||||
|
||||
29
src/pretix/api/migrations/0008_webhookcallretry.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 3.2.12 on 2022-09-13 14:48
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0218_checkinlist_addon_match'),
|
||||
('pretixapi', '0007_alter_webhookcall_target_url'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='WebHookCallRetry',
|
||||
fields=[
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('retry_not_before', models.DateTimeField(auto_now_add=True)),
|
||||
('retry_count', models.PositiveIntegerField(default=0)),
|
||||
('action_type', models.CharField(max_length=255)),
|
||||
('logentry', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='webhook_retries', to='pretixbase.logentry')),
|
||||
('webhook', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='retries', to='pretixapi.webhook')),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('webhook', 'logentry')},
|
||||
},
|
||||
),
|
||||
]
|
||||
77
src/pretix/api/migrations/0009_auto_20221217_1847.py
Normal file
@@ -0,0 +1,77 @@
|
||||
# Generated by Django 3.2.16 on 2022-12-17 18:47
|
||||
|
||||
import uuid
|
||||
|
||||
import django.db.models.deletion
|
||||
import oauth2_provider.generators
|
||||
import oauth2_provider.models
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0226_itemvariationmetavalue'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('pretixapi', '0008_webhookcallretry'),
|
||||
]
|
||||
run_before = [
|
||||
('oauth2_provider', '0002_auto_20190406_1805'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='oauthapplication',
|
||||
name='algorithm',
|
||||
field=models.CharField(default='', max_length=5),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='oauthgrant',
|
||||
name='claims',
|
||||
field=models.TextField(default=''),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='oauthgrant',
|
||||
name='code_challenge',
|
||||
field=models.CharField(default='', max_length=128),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='oauthgrant',
|
||||
name='code_challenge_method',
|
||||
field=models.CharField(default='', max_length=10),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='oauthgrant',
|
||||
name='nonce',
|
||||
field=models.CharField(default='', max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='oauthapplication',
|
||||
name='client_secret',
|
||||
field=oauth2_provider.models.ClientSecretField(db_index=True, default=oauth2_provider.generators.generate_client_secret, max_length=255),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='OAuthIDToken',
|
||||
fields=[
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('jti', models.UUIDField(default=uuid.uuid4, unique=True)),
|
||||
('expires', models.DateTimeField()),
|
||||
('scope', models.TextField()),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('updated', models.DateTimeField(auto_now=True)),
|
||||
('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
|
||||
('organizers', models.ManyToManyField(to='pretixbase.Organizer')),
|
||||
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='pretixapi_oauthidtoken', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='oauthaccesstoken',
|
||||
name='id_token',
|
||||
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='access_token', to='pretixapi.oauthidtoken'),
|
||||
),
|
||||
]
|
||||
@@ -29,8 +29,8 @@ from oauth2_provider.generators import (
|
||||
generate_client_id, generate_client_secret,
|
||||
)
|
||||
from oauth2_provider.models import (
|
||||
AbstractAccessToken, AbstractApplication, AbstractGrant,
|
||||
AbstractRefreshToken,
|
||||
AbstractAccessToken, AbstractApplication, AbstractGrant, AbstractIDToken,
|
||||
AbstractRefreshToken, ClientSecretField,
|
||||
)
|
||||
from oauth2_provider.validators import URIValidator
|
||||
|
||||
@@ -46,7 +46,7 @@ class OAuthApplication(AbstractApplication):
|
||||
verbose_name=_("Client ID"),
|
||||
max_length=100, unique=True, default=generate_client_id, db_index=True
|
||||
)
|
||||
client_secret = models.CharField(
|
||||
client_secret = ClientSecretField(
|
||||
verbose_name=_("Client secret"),
|
||||
max_length=255, blank=False, default=generate_client_secret, db_index=True
|
||||
)
|
||||
@@ -67,12 +67,26 @@ class OAuthGrant(AbstractGrant):
|
||||
redirect_uri = models.CharField(max_length=2500) # Only 255 in AbstractGrant, which caused problems
|
||||
|
||||
|
||||
class OAuthIDToken(AbstractIDToken):
|
||||
application = models.ForeignKey(
|
||||
OAuthApplication, on_delete=models.CASCADE,
|
||||
)
|
||||
organizers = models.ManyToManyField('pretixbase.Organizer')
|
||||
|
||||
|
||||
class OAuthAccessToken(AbstractAccessToken):
|
||||
source_refresh_token = models.OneToOneField(
|
||||
# unique=True implied by the OneToOneField
|
||||
'OAuthRefreshToken', on_delete=models.SET_NULL, blank=True, null=True,
|
||||
related_name="refreshed_access_token"
|
||||
)
|
||||
id_token = models.OneToOneField(
|
||||
OAuthIDToken,
|
||||
on_delete=models.CASCADE,
|
||||
blank=True,
|
||||
null=True,
|
||||
related_name="access_token",
|
||||
)
|
||||
application = models.ForeignKey(
|
||||
OAuthApplication, on_delete=models.CASCADE, blank=True, null=True,
|
||||
)
|
||||
@@ -133,6 +147,18 @@ class WebHookCall(models.Model):
|
||||
ordering = ("-datetime",)
|
||||
|
||||
|
||||
class WebHookCallRetry(models.Model):
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
webhook = models.ForeignKey('WebHook', on_delete=models.CASCADE, related_name='retries')
|
||||
logentry = models.ForeignKey('pretixbase.LogEntry', on_delete=models.CASCADE, related_name='webhook_retries')
|
||||
retry_not_before = models.DateTimeField(auto_now_add=True)
|
||||
retry_count = models.PositiveIntegerField(default=0)
|
||||
action_type = models.CharField(max_length=255)
|
||||
|
||||
class Meta:
|
||||
unique_together = (('webhook', 'logentry'),)
|
||||
|
||||
|
||||
class ApiCall(models.Model):
|
||||
idempotency_key = models.CharField(max_length=190, db_index=True)
|
||||
auth_hash = models.CharField(max_length=190, db_index=True)
|
||||
|
||||
@@ -23,7 +23,7 @@ import os
|
||||
from datetime import timedelta
|
||||
|
||||
from django.core.files import File
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.db.models import prefetch_related_objects
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy
|
||||
from rest_framework import serializers
|
||||
@@ -33,120 +33,36 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.order import (
|
||||
AnswerCreateSerializer, AnswerSerializer, InlineSeatSerializer,
|
||||
)
|
||||
from pretix.base.models import Quota, Seat
|
||||
from pretix.base.models import Seat, Voucher
|
||||
from pretix.base.models.orders import CartPosition
|
||||
|
||||
|
||||
class TaxIncludedField(serializers.Field):
|
||||
def to_representation(self, instance: CartPosition):
|
||||
return not instance.custom_price_input_is_net
|
||||
|
||||
|
||||
class CartPositionSerializer(I18nAwareModelSerializer):
|
||||
answers = AnswerSerializer(many=True)
|
||||
seat = InlineSeatSerializer()
|
||||
includes_tax = TaxIncludedField(source='*')
|
||||
|
||||
class Meta:
|
||||
model = CartPosition
|
||||
fields = ('id', 'cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
||||
'attendee_email', 'voucher', 'addon_to', 'subevent', 'datetime', 'expires', 'includes_tax',
|
||||
'answers', 'seat')
|
||||
'answers', 'seat', 'is_bundled')
|
||||
|
||||
|
||||
class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
class BaseCartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
answers = AnswerCreateSerializer(many=True, required=False)
|
||||
expires = serializers.DateTimeField(required=False)
|
||||
attendee_name = serializers.CharField(required=False, allow_null=True)
|
||||
seat = serializers.CharField(required=False, allow_null=True)
|
||||
sales_channel = serializers.CharField(required=False, default='sales_channel')
|
||||
includes_tax = serializers.BooleanField(required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = CartPosition
|
||||
fields = ('cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
||||
'subevent', 'expires', 'includes_tax', 'answers', 'seat', 'sales_channel')
|
||||
|
||||
def create(self, validated_data):
|
||||
answers_data = validated_data.pop('answers')
|
||||
if not validated_data.get('cart_id'):
|
||||
cid = "{}@api".format(get_random_string(48))
|
||||
while CartPosition.objects.filter(cart_id=cid).exists():
|
||||
cid = "{}@api".format(get_random_string(48))
|
||||
validated_data['cart_id'] = cid
|
||||
|
||||
if not validated_data.get('expires'):
|
||||
validated_data['expires'] = now() + timedelta(
|
||||
minutes=self.context['event'].settings.get('reservation_time', as_type=int)
|
||||
)
|
||||
|
||||
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('There is not enough quota available on quota "{}" to perform '
|
||||
'the operation.').format(
|
||||
quota.name
|
||||
)
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
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')
|
||||
if isinstance(answ_data['answer'], File):
|
||||
an = answ_data.pop('answer')
|
||||
answ = cp.answers.create(**answ_data, answer='')
|
||||
answ.file.save(os.path.basename(an.name), an, save=False)
|
||||
answ.answer = 'file://' + answ.file.name
|
||||
answ.save()
|
||||
an.close()
|
||||
else:
|
||||
answ = cp.answers.create(**answ_data)
|
||||
answ.options.add(*options)
|
||||
return cp
|
||||
|
||||
def validate_cart_id(self, cid):
|
||||
if cid and not cid.endswith('@api'):
|
||||
raise ValidationError('Cart ID should end in @api or be empty.')
|
||||
return cid
|
||||
fields = ('item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
||||
'subevent', 'includes_tax', 'answers')
|
||||
|
||||
def validate_item(self, item):
|
||||
if item.event != self.context['event']:
|
||||
@@ -193,4 +109,180 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
raise ValidationError(
|
||||
{'attendee_name': ['Do not specify attendee_name if you specified attendee_name_parts.']}
|
||||
)
|
||||
|
||||
if not data.get('expires'):
|
||||
data['expires'] = now() + timedelta(
|
||||
minutes=self.context['event'].settings.get('reservation_time', as_type=int)
|
||||
)
|
||||
|
||||
quotas_for_item_cache = self.context.get('quotas_for_item_cache', {})
|
||||
quotas_for_variation_cache = self.context.get('quotas_for_variation_cache', {})
|
||||
|
||||
seated = data.get('item').seat_category_mappings.filter(subevent=data.get('subevent')).exists()
|
||||
if data.get('seat'):
|
||||
if not seated:
|
||||
raise ValidationError({'seat': ['The specified product does not allow to choose a seat.']})
|
||||
try:
|
||||
seat = self.context['event'].seats.get(seat_guid=data['seat'], subevent=data.get('subevent'))
|
||||
except Seat.DoesNotExist:
|
||||
raise ValidationError({'seat': ['The specified seat does not exist.']})
|
||||
except Seat.MultipleObjectsReturned:
|
||||
raise ValidationError({'seat': ['The specified seat ID is not unique.']})
|
||||
else:
|
||||
data['seat'] = seat
|
||||
elif seated:
|
||||
raise ValidationError({'seat': ['The specified product requires to choose a seat.']})
|
||||
|
||||
if data.get('voucher'):
|
||||
try:
|
||||
voucher = self.context['event'].vouchers.get(code__iexact=data['voucher'])
|
||||
except Voucher.DoesNotExist:
|
||||
raise ValidationError({'voucher': ['The specified voucher does not exist.']})
|
||||
|
||||
if voucher and not voucher.applies_to(data['item'], data.get('variation')):
|
||||
raise ValidationError({'voucher': ['The specified voucher is not valid for the given item and variation.']})
|
||||
|
||||
if voucher and voucher.seat and voucher.seat != data.get('seat'):
|
||||
raise ValidationError({'voucher': ['The specified voucher is not valid for this seat.']})
|
||||
|
||||
if voucher and voucher.subevent_id and (not data.get('subevent') or voucher.subevent_id != data['subevent'].pk):
|
||||
raise ValidationError({'voucher': ['The specified voucher is not valid for this subevent.']})
|
||||
|
||||
if voucher.valid_until is not None and voucher.valid_until < now():
|
||||
raise ValidationError({'voucher': ['The specified voucher is expired.']})
|
||||
|
||||
data['voucher'] = voucher
|
||||
|
||||
if not data.get('voucher') or (not data['voucher'].allow_ignore_quota and not data['voucher'].block_quota):
|
||||
if data.get('variation'):
|
||||
if data['variation'].pk not in quotas_for_variation_cache:
|
||||
quotas_for_variation_cache[data['variation'].pk] = data['variation'].quotas.filter(subevent=data.get('subevent'))
|
||||
data['_quotas'] = quotas_for_variation_cache[data['variation'].pk]
|
||||
else:
|
||||
if data['item'].pk not in quotas_for_item_cache:
|
||||
quotas_for_item_cache[data['item'].pk] = data['item'].quotas.filter(subevent=data.get('subevent'))
|
||||
data['_quotas'] = quotas_for_item_cache[data['item'].pk]
|
||||
|
||||
if len(data['_quotas']) == 0:
|
||||
raise ValidationError(
|
||||
gettext_lazy('The product "{}" is not assigned to a quota.').format(
|
||||
str(data.get('item'))
|
||||
)
|
||||
)
|
||||
else:
|
||||
data['_quotas'] = []
|
||||
|
||||
return data
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data.pop('_quotas')
|
||||
answers_data = validated_data.pop('answers')
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
# todo: does this make sense?
|
||||
validated_data['custom_price_input'] = validated_data['price']
|
||||
# todo: listed price, etc?
|
||||
# currently does not matter because there is no way to transform an API cart position into an order that keeps
|
||||
# prices, cart positions are just quota/voucher placeholders
|
||||
validated_data['custom_price_input_is_net'] = not validated_data.pop('includes_tax', True)
|
||||
cp = CartPosition.objects.create(event=self.context['event'], **validated_data)
|
||||
|
||||
for answ_data in answers_data:
|
||||
options = answ_data.pop('options')
|
||||
if isinstance(answ_data['answer'], File):
|
||||
an = answ_data.pop('answer')
|
||||
answ = cp.answers.create(**answ_data, answer='')
|
||||
answ.file.save(os.path.basename(an.name), an, save=False)
|
||||
answ.answer = 'file://' + answ.file.name
|
||||
answ.save()
|
||||
an.close()
|
||||
else:
|
||||
answ = cp.answers.create(**answ_data)
|
||||
answ.options.add(*options)
|
||||
return cp
|
||||
|
||||
|
||||
class CartPositionCreateSerializer(BaseCartPositionCreateSerializer):
|
||||
expires = serializers.DateTimeField(required=False)
|
||||
addons = BaseCartPositionCreateSerializer(many=True, required=False)
|
||||
bundled = BaseCartPositionCreateSerializer(many=True, required=False)
|
||||
seat = serializers.CharField(required=False, allow_null=True)
|
||||
sales_channel = serializers.CharField(required=False, default='sales_channel')
|
||||
voucher = serializers.CharField(required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = CartPosition
|
||||
fields = BaseCartPositionCreateSerializer.Meta.fields + (
|
||||
'cart_id', 'expires', 'addons', 'bundled', 'seat', 'sales_channel', 'voucher'
|
||||
)
|
||||
|
||||
def validate_cart_id(self, cid):
|
||||
if cid and not cid.endswith('@api'):
|
||||
raise ValidationError('Cart ID should end in @api or be empty.')
|
||||
return cid
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data.pop('sales_channel')
|
||||
addons_data = validated_data.pop('addons', None)
|
||||
bundled_data = validated_data.pop('bundled', None)
|
||||
|
||||
cp = super().create(validated_data)
|
||||
|
||||
if addons_data:
|
||||
for addon_data in addons_data:
|
||||
addon_data['addon_to'] = cp
|
||||
addon_data['is_bundled'] = False
|
||||
addon_data['cart_id'] = cp.cart_id
|
||||
super().create(addon_data)
|
||||
|
||||
if bundled_data:
|
||||
for bundle_data in bundled_data:
|
||||
bundle_data['addon_to'] = cp
|
||||
bundle_data['is_bundled'] = True
|
||||
bundle_data['cart_id'] = cp.cart_id
|
||||
super().create(bundle_data)
|
||||
|
||||
return cp
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
|
||||
# This is currently only a very basic validation of add-ons and bundled products, we don't validate their number
|
||||
# or price. We can always go stricter, as the endpoint is documented as experimental.
|
||||
# However, this serializer should always be *at least* as strict as the order creation serializer.
|
||||
|
||||
if data.get('item') and data.get('addons'):
|
||||
prefetch_related_objects([data['item']], 'addons')
|
||||
for sub_data in data['addons']:
|
||||
if not any(a.addon_category_id == sub_data['item'].category_id for a in data['item'].addons.all()):
|
||||
raise ValidationError({
|
||||
'addons': [
|
||||
'The product "{prod}" can not be used as an add-on product for "{main}".'.format(
|
||||
prod=str(sub_data['item']),
|
||||
main=str(data['item']),
|
||||
)
|
||||
]
|
||||
})
|
||||
|
||||
if data.get('item') and data.get('bundled'):
|
||||
prefetch_related_objects([data['item']], 'bundles')
|
||||
for sub_data in data['bundled']:
|
||||
if not any(
|
||||
a.bundled_item_id == sub_data['item'].pk and
|
||||
a.bundled_variation_id == (sub_data['variation'].pk if sub_data.get('variation') else None)
|
||||
for a in data['item'].bundles.all()
|
||||
):
|
||||
raise ValidationError({
|
||||
'bundled': [
|
||||
'The product "{prod}" can not be used as an bundled product for "{main}".'.format(
|
||||
prod=str(sub_data['item']),
|
||||
main=str(data['item']),
|
||||
)
|
||||
]
|
||||
})
|
||||
return data
|
||||
|
||||
@@ -26,7 +26,7 @@ from rest_framework.exceptions import ValidationError
|
||||
from pretix.api.serializers.event import SubEventSerializer
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.models import CheckinList
|
||||
from pretix.base.models import Checkin, CheckinList
|
||||
|
||||
|
||||
class CheckinListSerializer(I18nAwareModelSerializer):
|
||||
@@ -37,7 +37,7 @@ class CheckinListSerializer(I18nAwareModelSerializer):
|
||||
model = CheckinList
|
||||
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count',
|
||||
'include_pending', 'auto_checkin_sales_channels', 'allow_multiple_entries', 'allow_entry_after_exit',
|
||||
'rules', 'exit_all_at')
|
||||
'rules', 'exit_all_at', 'addon_match')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -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.'))
|
||||
|
||||
@@ -78,3 +78,31 @@ class CheckinListSerializer(I18nAwareModelSerializer):
|
||||
CheckinList.validate_rules(data.get('rules'))
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class CheckinRPCRedeemInputSerializer(serializers.Serializer):
|
||||
lists = serializers.PrimaryKeyRelatedField(required=True, many=True, queryset=CheckinList.objects.none())
|
||||
secret = serializers.CharField(required=True, allow_null=False)
|
||||
force = serializers.BooleanField(default=False, required=False)
|
||||
type = serializers.ChoiceField(choices=Checkin.CHECKIN_TYPES, default=Checkin.TYPE_ENTRY)
|
||||
ignore_unpaid = serializers.BooleanField(default=False, required=False)
|
||||
questions_supported = serializers.BooleanField(default=True, required=False)
|
||||
nonce = serializers.CharField(required=False, allow_null=True)
|
||||
datetime = serializers.DateTimeField(required=False, allow_null=True)
|
||||
answers = serializers.JSONField(required=False, allow_null=True)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['lists'].child_relation.queryset = CheckinList.objects.filter(event__in=self.context['events']).select_related('event')
|
||||
|
||||
|
||||
class MiniCheckinListSerializer(I18nAwareModelSerializer):
|
||||
event = serializers.SlugRelatedField(slug_field='slug', read_only=True)
|
||||
subevent = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = CheckinList
|
||||
fields = ('id', 'name', 'event', 'subevent', 'include_pending')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
49
src/pretix/api/serializers/discount.py
Normal file
@@ -0,0 +1,49 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.models import Discount
|
||||
|
||||
|
||||
class DiscountSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Discount
|
||||
fields = ('id', 'active', 'internal_name', 'position', 'sales_channels', 'available_from',
|
||||
'available_until', 'subevent_mode', 'condition_all_products', 'condition_limit_products',
|
||||
'condition_apply_to_addons', 'condition_min_count', 'condition_min_value',
|
||||
'benefit_discount_matching_percent', 'benefit_only_apply_to_cheapest_n_matches',
|
||||
'condition_ignore_voucher_discounted')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['condition_limit_products'].queryset = self.context['event'].items.all()
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
|
||||
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
||||
full_data.update(data)
|
||||
|
||||
Discount.validate_config(full_data)
|
||||
|
||||
return data
|
||||
@@ -54,7 +54,10 @@ 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 LazyI18nStringList, validate_event_settings
|
||||
from pretix.base.settings import (
|
||||
PERSON_NAME_SALUTATIONS, PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS,
|
||||
LazyI18nStringList, validate_event_settings,
|
||||
)
|
||||
from pretix.base.signals import api_event_settings_fields
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -408,7 +411,8 @@ class CloneEventSerializer(EventSerializer):
|
||||
has_subevents = validated_data.pop('has_subevents', None)
|
||||
tz = validated_data.pop('timezone', None)
|
||||
sales_channels = validated_data.pop('sales_channels', None)
|
||||
new_event = super().create(validated_data)
|
||||
date_admission = validated_data.pop('date_admission', None)
|
||||
new_event = super().create({**validated_data, 'plugins': None})
|
||||
|
||||
event = Event.objects.filter(slug=self.context['event'], organizer=self.context['organizer'].pk).first()
|
||||
new_event.copy_data_from(event)
|
||||
@@ -423,6 +427,10 @@ class CloneEventSerializer(EventSerializer):
|
||||
new_event.sales_channels = sales_channels
|
||||
if has_subevents is not None:
|
||||
new_event.has_subevents = has_subevents
|
||||
if has_subevents is not None:
|
||||
new_event.has_subevents = has_subevents
|
||||
if date_admission is not None:
|
||||
new_event.date_admission = date_admission
|
||||
new_event.save()
|
||||
if tz:
|
||||
new_event.settings.timezone = tz
|
||||
@@ -637,7 +645,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):
|
||||
@@ -713,7 +721,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',
|
||||
@@ -753,6 +760,9 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'invoice_logo_image',
|
||||
'cancel_allow_user',
|
||||
'cancel_allow_user_until',
|
||||
'cancel_allow_user_unpaid_keep',
|
||||
'cancel_allow_user_unpaid_keep_fees',
|
||||
'cancel_allow_user_unpaid_keep_percentage',
|
||||
'cancel_allow_user_paid',
|
||||
'cancel_allow_user_paid_until',
|
||||
'cancel_allow_user_paid_keep',
|
||||
@@ -763,6 +773,7 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'cancel_allow_user_paid_adjust_fees_step',
|
||||
'cancel_allow_user_paid_refund_as_giftcard',
|
||||
'cancel_allow_user_paid_require_approval',
|
||||
'cancel_allow_user_paid_require_approval_fee_unknown',
|
||||
'change_allow_user_variation',
|
||||
'change_allow_user_addons',
|
||||
'change_allow_user_until',
|
||||
@@ -777,6 +788,7 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'logo_image_large',
|
||||
'logo_show_title',
|
||||
'og_image',
|
||||
'name_scheme',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -842,4 +854,25 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
|
||||
'invoice_address_from_country',
|
||||
'invoice_address_from_tax_id',
|
||||
'invoice_address_from_vat_id',
|
||||
'name_scheme',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['_name_scheme_fields'] = serializers.JSONField(
|
||||
read_only=True,
|
||||
default=[{"key": k, "label": str(v), "weight": w} for k, v, w, *__ in PERSON_NAME_SCHEMES.get(self.event.settings.name_scheme)['fields']]
|
||||
)
|
||||
self.fields['_name_scheme_salutations'] = serializers.JSONField(
|
||||
read_only=True,
|
||||
default=[{"key": k, "label": str(v)} for k, v in PERSON_NAME_SALUTATIONS]
|
||||
)
|
||||
self.fields['_name_scheme_titles'] = serializers.JSONField(
|
||||
read_only=True,
|
||||
default=(
|
||||
[{"key": k, "label": k}
|
||||
for k in PERSON_NAME_TITLE_GROUPS.get(self.event.settings.name_scheme_titles)[1]]
|
||||
if self.event.settings.name_scheme_titles
|
||||
else []
|
||||
)
|
||||
)
|
||||
|
||||
@@ -23,6 +23,8 @@ from django import forms
|
||||
from django.http import QueryDict
|
||||
from rest_framework import serializers
|
||||
|
||||
from pretix.base.exporter import OrganizerLevelExportMixin
|
||||
|
||||
|
||||
class FormFieldWrapperField(serializers.Field):
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -49,7 +51,6 @@ simple_mappings = (
|
||||
(forms.EmailField, serializers.EmailField, ()),
|
||||
(forms.UUIDField, serializers.UUIDField, ()),
|
||||
(forms.URLField, serializers.URLField, ()),
|
||||
(forms.NullBooleanField, serializers.NullBooleanField, ()),
|
||||
(forms.BooleanField, serializers.BooleanField, ()),
|
||||
)
|
||||
|
||||
@@ -87,7 +88,7 @@ class JobRunSerializer(serializers.Serializer):
|
||||
ex = kwargs.pop('exporter')
|
||||
events = kwargs.pop('events', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
if events is not None:
|
||||
if events is not None and not isinstance(ex, OrganizerLevelExportMixin):
|
||||
self.fields["events"] = serializers.SlugRelatedField(
|
||||
queryset=events,
|
||||
required=True,
|
||||
@@ -106,6 +107,12 @@ class JobRunSerializer(serializers.Serializer):
|
||||
)
|
||||
break
|
||||
|
||||
if isinstance(v, forms.NullBooleanField):
|
||||
self.fields[k] = serializers.BooleanField(
|
||||
required=v.required,
|
||||
allow_null=True,
|
||||
validators=v.validators,
|
||||
)
|
||||
if isinstance(v, forms.ModelMultipleChoiceField):
|
||||
self.fields[k] = PrimaryKeyRelatedField(
|
||||
queryset=v.queryset,
|
||||
|
||||
@@ -47,41 +47,102 @@ from pretix.api.serializers.fields import UploadedFileField
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.models import (
|
||||
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemVariation,
|
||||
Question, QuestionOption, Quota,
|
||||
ItemVariationMetaValue, Question, QuestionOption, Quota,
|
||||
)
|
||||
|
||||
|
||||
class InlineItemVariationSerializer(I18nAwareModelSerializer):
|
||||
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=10,
|
||||
coerce_to_string=True)
|
||||
meta_data = MetaDataField(required=False, source='*')
|
||||
|
||||
class Meta:
|
||||
model = ItemVariation
|
||||
fields = ('id', 'value', 'active', 'description',
|
||||
'position', 'default_price', 'price', 'original_price',
|
||||
'require_membership', 'require_membership_types', 'require_membership_hidden', 'available_from', 'available_until',
|
||||
'sales_channels', 'hide_without_voucher',)
|
||||
'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', 'meta_data')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['require_membership_types'].queryset = lazy(lambda: self.context['event'].organizer.membership_types.all(), QuerySet)
|
||||
|
||||
def validate_meta_data(self, value):
|
||||
for key in value['meta_data'].keys():
|
||||
if key not in self.parent.parent.item_meta_properties:
|
||||
raise ValidationError(_('Item meta data property \'{name}\' does not exist.').format(name=key))
|
||||
return value
|
||||
|
||||
|
||||
class ItemVariationSerializer(I18nAwareModelSerializer):
|
||||
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=10,
|
||||
coerce_to_string=True)
|
||||
meta_data = MetaDataField(required=False, source='*')
|
||||
|
||||
class Meta:
|
||||
model = ItemVariation
|
||||
fields = ('id', 'value', 'active', 'description',
|
||||
'position', 'default_price', 'price', 'original_price',
|
||||
'require_membership', 'require_membership_types', 'require_membership_hidden', 'available_from', 'available_until',
|
||||
'sales_channels', 'hide_without_voucher',)
|
||||
'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', 'meta_data')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['require_membership_types'].queryset = self.context['event'].organizer.membership_types.all()
|
||||
|
||||
@transaction.atomic
|
||||
def create(self, validated_data):
|
||||
meta_data = validated_data.pop('meta_data', None)
|
||||
variation = ItemVariation.objects.create(**validated_data)
|
||||
|
||||
# Meta data
|
||||
if meta_data is not None:
|
||||
for key, value in meta_data.items():
|
||||
ItemVariationMetaValue.objects.create(
|
||||
property=self.item_meta_properties.get(key),
|
||||
value=value,
|
||||
variation=variation
|
||||
)
|
||||
return variation
|
||||
|
||||
@cached_property
|
||||
def item_meta_properties(self):
|
||||
return {
|
||||
p.name: p for p in self.context['request'].event.item_meta_properties.all()
|
||||
}
|
||||
|
||||
def validate_meta_data(self, value):
|
||||
for key in value['meta_data'].keys():
|
||||
if key not in self.item_meta_properties:
|
||||
raise ValidationError(_('Item meta data property \'{name}\' does not exist.').format(name=key))
|
||||
return value
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
meta_data = validated_data.pop('meta_data', None)
|
||||
variation = super().update(instance, validated_data)
|
||||
|
||||
# Meta data
|
||||
if meta_data is not None:
|
||||
current = {mv.property: mv for mv in variation.meta_values.select_related('property')}
|
||||
for key, value in meta_data.items():
|
||||
prop = self.item_meta_properties.get(key)
|
||||
if prop in current:
|
||||
current[prop].value = value
|
||||
current[prop].save()
|
||||
else:
|
||||
variation.meta_values.create(
|
||||
property=self.item_meta_properties.get(key),
|
||||
value=value
|
||||
)
|
||||
|
||||
for prop, current_object in current.items():
|
||||
if prop.name not in meta_data:
|
||||
current_object.delete()
|
||||
|
||||
return variation
|
||||
|
||||
|
||||
class InlineItemBundleSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
@@ -182,8 +243,11 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['require_membership_types'].queryset = self.context['event'].organizer.membership_types.all()
|
||||
self.fields['grant_membership_type'].queryset = self.context['event'].organizer.membership_types.all()
|
||||
self.fields['default_price'].allow_null = False
|
||||
self.fields['default_price'].required = True
|
||||
if not self.read_only:
|
||||
self.fields['require_membership_types'].queryset = self.context['event'].organizer.membership_types.all()
|
||||
self.fields['grant_membership_type'].queryset = self.context['event'].organizer.membership_types.all()
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
@@ -249,15 +313,28 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
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', [])
|
||||
var_meta_data = variation_data.pop('meta_data', {})
|
||||
v = ItemVariation.objects.create(item=item, **variation_data)
|
||||
if require_membership_types:
|
||||
v.require_membership_types.add(*require_membership_types)
|
||||
|
||||
if var_meta_data is not None:
|
||||
for key, value in var_meta_data.items():
|
||||
ItemVariationMetaValue.objects.create(
|
||||
property=self.item_meta_properties.get(key),
|
||||
value=value,
|
||||
variation=v
|
||||
)
|
||||
|
||||
for addon_data in addons_data:
|
||||
ItemAddOn.objects.create(base_item=item, **addon_data)
|
||||
for bundle_data in bundles_data:
|
||||
|
||||
@@ -29,6 +29,7 @@ import pycountry
|
||||
from django.conf import settings
|
||||
from django.core.files import File
|
||||
from django.db.models import F, Q
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy
|
||||
from django_countries.fields import Country
|
||||
@@ -56,17 +57,30 @@ from pretix.base.models.orders import (
|
||||
from pretix.base.pdf import get_images, get_variables
|
||||
from pretix.base.services.cart import error_messages
|
||||
from pretix.base.services.locking import NoLockManager
|
||||
from pretix.base.services.pricing import get_price
|
||||
from pretix.base.services.pricing import (
|
||||
apply_discounts, get_line_price, get_listed_price, is_included_for_free,
|
||||
)
|
||||
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
|
||||
from pretix.base.signals import register_ticket_outputs
|
||||
from pretix.helpers.countries import CachedCountries
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CompatibleCountryField(serializers.Field):
|
||||
countries = CachedCountries()
|
||||
default_error_messages = {
|
||||
'invalid_choice': gettext_lazy('"{input}" is not a valid choice.')
|
||||
}
|
||||
|
||||
def to_internal_value(self, data):
|
||||
return {self.field_name: Country(data)}
|
||||
country = self.countries.alpha2(data)
|
||||
if data and not country:
|
||||
country = self.countries.by_name(force_str(data))
|
||||
if not country:
|
||||
self.fail("invalid_choice", input=data)
|
||||
return {self.field_name: Country(country)}
|
||||
|
||||
def to_representation(self, instance: InvoiceAddress):
|
||||
if instance.country:
|
||||
@@ -90,7 +104,7 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = InvoiceAddress
|
||||
fields = ('last_modified', 'is_business', 'company', 'name', 'name_parts', 'street', 'zipcode', 'city', 'country',
|
||||
'state', 'vat_id', 'vat_id_validated', 'internal_reference')
|
||||
'state', 'vat_id', 'vat_id_validated', 'custom_field', 'internal_reference')
|
||||
read_only_fields = ('last_modified',)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -339,10 +353,10 @@ class PdfDataSerializer(serializers.Field):
|
||||
# we serialize a list.
|
||||
|
||||
if 'vars' not in self.context:
|
||||
self.context['vars'] = get_variables(self.context['request'].event)
|
||||
self.context['vars'] = get_variables(self.context['event'])
|
||||
|
||||
if 'vars_images' not in self.context:
|
||||
self.context['vars_images'] = get_images(self.context['request'].event)
|
||||
self.context['vars_images'] = get_images(self.context['event'])
|
||||
|
||||
for k, f in self.context['vars'].items():
|
||||
try:
|
||||
@@ -357,10 +371,19 @@ class PdfDataSerializer(serializers.Field):
|
||||
for k, v in ev._cached_meta_data.items():
|
||||
res['meta:' + k] = v
|
||||
|
||||
if not hasattr(instance.item, '_cached_meta_data'):
|
||||
instance.item._cached_meta_data = instance.item.meta_data
|
||||
for k, v in instance.item._cached_meta_data.items():
|
||||
res['itemmeta:' + k] = v
|
||||
if instance.variation_id:
|
||||
print(instance, instance.variation, instance.variation_id, instance.item)
|
||||
if not hasattr(instance.variation, '_cached_meta_data'):
|
||||
instance.variation.item = instance.item # saves some database lookups
|
||||
instance.variation._cached_meta_data = instance.variation.meta_data
|
||||
print(instance.variation._cached_meta_data.items())
|
||||
for k, v in instance.variation._cached_meta_data.items():
|
||||
res['itemmeta:' + k] = v
|
||||
else:
|
||||
if not hasattr(instance.item, '_cached_meta_data'):
|
||||
instance.item._cached_meta_data = instance.item.meta_data
|
||||
for k, v in instance.item._cached_meta_data.items():
|
||||
res['itemmeta:' + k] = v
|
||||
|
||||
res['images'] = {}
|
||||
|
||||
@@ -408,104 +431,30 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
||||
'company', 'street', 'zipcode', 'city', 'country', 'state',
|
||||
'company', 'street', 'zipcode', 'city', 'country', 'state', 'discount',
|
||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled')
|
||||
read_only_fields = (
|
||||
'id', 'order', 'positionid', 'item', 'variation', 'price', 'voucher', 'tax_rate', 'tax_value', 'secret',
|
||||
'addon_to', 'subevent', 'checkins', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data',
|
||||
'seat', 'canceled'
|
||||
'seat', 'canceled', 'discount',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
request = self.context.get('request')
|
||||
if request and (not request.query_params.get('pdf_data', 'false') == 'true' or 'can_view_orders' not in request.eventpermset):
|
||||
pdf_data_forbidden = (
|
||||
# We check this based on permission if we are on /events/…/orders/ or /events/…/orderpositions/ or
|
||||
# /events/…/checkinlists/…/positions/
|
||||
# We're unable to check this on this level if we're on /checkinrpc/, in which case we rely on the view
|
||||
# layer to not set pdf_data=true in the first place.
|
||||
request and hasattr(request, 'event') and 'can_view_orders' not in request.eventpermset
|
||||
)
|
||||
if ('pdf_data' in self.context and not self.context['pdf_data']) or pdf_data_forbidden:
|
||||
self.fields.pop('pdf_data', None)
|
||||
|
||||
def validate(self, data):
|
||||
if data.get('attendee_name') and data.get('attendee_name_parts'):
|
||||
raise ValidationError(
|
||||
{'attendee_name': ['Do not specify attendee_name if you specified attendee_name_parts.']}
|
||||
)
|
||||
if data.get('attendee_name_parts') and '_scheme' not in data.get('attendee_name_parts'):
|
||||
data['attendee_name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
|
||||
|
||||
if data.get('country'):
|
||||
if not pycountry.countries.get(alpha_2=data.get('country').code):
|
||||
raise ValidationError(
|
||||
{'country': ['Invalid country code.']}
|
||||
)
|
||||
|
||||
if data.get('state'):
|
||||
cc = str(data.get('country') or self.instance.country or '')
|
||||
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||
raise ValidationError(
|
||||
{'state': ['States are not supported in country "{}".'.format(cc)]}
|
||||
)
|
||||
if not pycountry.subdivisions.get(code=cc + '-' + data.get('state')):
|
||||
raise ValidationError(
|
||||
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
|
||||
)
|
||||
return data
|
||||
|
||||
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 = [
|
||||
'attendee_name_parts', 'company', 'street', 'zipcode', 'city', 'country',
|
||||
'state', 'attendee_email',
|
||||
]
|
||||
answers_data = validated_data.pop('answers', None)
|
||||
|
||||
name = validated_data.pop('attendee_name', '')
|
||||
if name and not validated_data.get('attendee_name_parts'):
|
||||
validated_data['attendee_name_parts'] = {
|
||||
'_legacy': name
|
||||
}
|
||||
|
||||
for attr, value in validated_data.items():
|
||||
if attr in update_fields:
|
||||
setattr(instance, attr, value)
|
||||
|
||||
instance.save(update_fields=update_fields)
|
||||
|
||||
if answers_data is not None:
|
||||
qs_seen = set()
|
||||
answercache = {
|
||||
a.question_id: a for a in instance.answers.all()
|
||||
}
|
||||
for answ_data in answers_data:
|
||||
options = answ_data.pop('options', [])
|
||||
if answ_data['question'].pk in qs_seen:
|
||||
raise ValidationError(f'Question {answ_data["question"]} was sent twice.')
|
||||
if answ_data['question'].pk in answercache:
|
||||
a = answercache[answ_data['question'].pk]
|
||||
if isinstance(answ_data['answer'], File):
|
||||
a.file.save(answ_data['answer'].name, answ_data['answer'], save=False)
|
||||
a.answer = 'file://' + a.file.name
|
||||
elif a.answer.startswith('file://') and answ_data['answer'] == "file:keep":
|
||||
pass # keep current file
|
||||
else:
|
||||
for attr, value in answ_data.items():
|
||||
setattr(a, attr, value)
|
||||
a.save()
|
||||
else:
|
||||
if isinstance(answ_data['answer'], File):
|
||||
an = answ_data.pop('answer')
|
||||
a = instance.answers.create(**answ_data, answer='')
|
||||
a.file.save(os.path.basename(an.name), an, save=False)
|
||||
a.answer = 'file://' + a.file.name
|
||||
a.save()
|
||||
else:
|
||||
a = instance.answers.create(**answ_data)
|
||||
a.options.set(options)
|
||||
qs_seen.add(a.question_id)
|
||||
for qid, a in answercache.items():
|
||||
if qid not in qs_seen:
|
||||
a.delete()
|
||||
|
||||
return instance
|
||||
raise TypeError("this serializer is readonly")
|
||||
|
||||
|
||||
class RequireAttentionField(serializers.Field):
|
||||
@@ -560,13 +509,13 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if 'subevent' in self.context['request'].query_params.getlist('expand'):
|
||||
if 'subevent' in self.context['expand']:
|
||||
self.fields['subevent'] = SubEventSerializer(read_only=True)
|
||||
|
||||
if 'item' in self.context['request'].query_params.getlist('expand'):
|
||||
if 'item' in self.context['expand']:
|
||||
self.fields['item'] = ItemSerializer(read_only=True, context=self.context)
|
||||
|
||||
if 'variation' in self.context['request'].query_params.getlist('expand'):
|
||||
if 'variation' in self.context['expand']:
|
||||
self.fields['variation'] = InlineItemVariationSerializer(read_only=True)
|
||||
|
||||
|
||||
@@ -593,7 +542,7 @@ class OrderPaymentDateField(serializers.DateField):
|
||||
class OrderFeeSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = OrderFee
|
||||
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule', 'canceled')
|
||||
fields = ('id', 'fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule', 'canceled')
|
||||
|
||||
|
||||
class PaymentURLField(serializers.URLField):
|
||||
@@ -625,12 +574,22 @@ class OrderPaymentSerializer(I18nAwareModelSerializer):
|
||||
'details')
|
||||
|
||||
|
||||
class RefundDetailsField(serializers.Field):
|
||||
def to_representation(self, value: OrderRefund):
|
||||
pp = value.payment_provider
|
||||
if not pp:
|
||||
return {}
|
||||
return pp.api_refund_details(value)
|
||||
|
||||
|
||||
class OrderRefundSerializer(I18nAwareModelSerializer):
|
||||
payment = SlugRelatedField(slug_field='local_id', read_only=True)
|
||||
details = RefundDetailsField(source='*', allow_null=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = OrderRefund
|
||||
fields = ('local_id', 'state', 'source', 'amount', 'payment', 'created', 'execution_date', 'comment', 'provider')
|
||||
fields = ('local_id', 'state', 'source', 'amount', 'payment', 'created', 'execution_date', 'comment', 'provider',
|
||||
'details')
|
||||
|
||||
|
||||
class OrderURLField(serializers.URLField):
|
||||
@@ -669,10 +628,36 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if not self.context['request'].query_params.get('pdf_data', 'false') == 'true':
|
||||
if not self.context['pdf_data']:
|
||||
self.fields['positions'].child.fields.pop('pdf_data', None)
|
||||
|
||||
for exclude_field in self.context['request'].query_params.getlist('exclude'):
|
||||
includes = set(self.context['include'])
|
||||
if includes:
|
||||
for fname, field in list(self.fields.items()):
|
||||
if fname in includes:
|
||||
continue
|
||||
elif hasattr(field, 'child'): # Nested list serializers
|
||||
found_any = False
|
||||
for childfname, childfield in list(field.child.fields.items()):
|
||||
if f'{fname}.{childfname}' not in includes:
|
||||
field.child.fields.pop(childfname)
|
||||
else:
|
||||
found_any = True
|
||||
if not found_any:
|
||||
self.fields.pop(fname)
|
||||
elif isinstance(field, serializers.Serializer): # Nested serializers
|
||||
found_any = False
|
||||
for childfname, childfield in list(field.fields.items()):
|
||||
if f'{fname}.{childfname}' not in includes:
|
||||
field.fields.pop(childfname)
|
||||
else:
|
||||
found_any = True
|
||||
if not found_any:
|
||||
self.fields.pop(fname)
|
||||
else:
|
||||
self.fields.pop(fname)
|
||||
|
||||
for exclude_field in self.context['exclude']:
|
||||
p = exclude_field.split('.')
|
||||
if p[0] in self.fields:
|
||||
if len(p) == 1:
|
||||
@@ -793,7 +778,7 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
||||
'company', 'street', 'zipcode', 'city', 'country', 'state',
|
||||
'company', 'street', 'zipcode', 'city', 'country', 'state', 'is_bundled',
|
||||
'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -934,7 +919,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
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)
|
||||
|
||||
@@ -947,7 +933,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', 'custom_followup_at')
|
||||
'force', 'send_email', 'simulate', 'customer', 'custom_followup_at', 'require_approval')
|
||||
|
||||
def validate_payment_provider(self, pp):
|
||||
if pp is None:
|
||||
@@ -1041,6 +1027,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')
|
||||
@@ -1053,8 +1041,18 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
else:
|
||||
ia = None
|
||||
|
||||
lock_required = False
|
||||
for pos_data in positions_data:
|
||||
pos_data['_quotas'] = list(
|
||||
pos_data.get('variation').quotas.filter(subevent=pos_data.get('subevent'))
|
||||
if pos_data.get('variation')
|
||||
else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent'))
|
||||
)
|
||||
if pos_data.get('voucher') or pos_data.get('seat') or any(q.size is not None for q in pos_data['_quotas']):
|
||||
lock_required = True
|
||||
|
||||
lockfn = self.context['event'].lock
|
||||
if simulate:
|
||||
if simulate or not lock_required:
|
||||
lockfn = NoLockManager
|
||||
with lockfn() as now_dt:
|
||||
free_seats = set()
|
||||
@@ -1118,29 +1116,18 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
|
||||
if v.budget is not None:
|
||||
price = pos_data.get('price')
|
||||
listed_price = get_listed_price(pos_data.get('item'), pos_data.get('variation'), pos_data.get('subevent'))
|
||||
|
||||
if pos_data.get('voucher'):
|
||||
price_after_voucher = pos_data.get('voucher').calculate_price(listed_price)
|
||||
else:
|
||||
price_after_voucher = listed_price
|
||||
if price is None:
|
||||
price = get_price(
|
||||
item=pos_data.get('item'),
|
||||
variation=pos_data.get('variation'),
|
||||
voucher=v,
|
||||
custom_price=None,
|
||||
subevent=pos_data.get('subevent'),
|
||||
addon_to=pos_data.get('addon_to'),
|
||||
invoice_address=ia,
|
||||
).gross
|
||||
pbv = get_price(
|
||||
item=pos_data['item'],
|
||||
variation=pos_data.get('variation'),
|
||||
voucher=None,
|
||||
custom_price=None,
|
||||
subevent=pos_data.get('subevent'),
|
||||
addon_to=pos_data.get('addon_to'),
|
||||
invoice_address=ia,
|
||||
)
|
||||
price = price_after_voucher
|
||||
|
||||
if v not in v_budget:
|
||||
v_budget[v] = v.budget - v.budget_used()
|
||||
disc = pbv.gross - price
|
||||
disc = max(listed_price - price, 0)
|
||||
if disc > v_budget[v]:
|
||||
new_disc = v_budget[v]
|
||||
v_budget[v] -= new_disc
|
||||
@@ -1156,6 +1143,10 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
|
||||
seated = pos_data.get('item').seat_category_mappings.filter(subevent=pos_data.get('subevent')).exists()
|
||||
if pos_data.get('seat'):
|
||||
if pos_data.get('addon_to'):
|
||||
errs[i]['seat'] = ['Seats are currently not supported for add-on products.']
|
||||
continue
|
||||
|
||||
if not seated:
|
||||
errs[i]['seat'] = ['The specified product does not allow to choose a seat.']
|
||||
try:
|
||||
@@ -1189,9 +1180,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
str(pos_data.get('item'))
|
||||
)]
|
||||
|
||||
new_quotas = (pos_data.get('variation').quotas.filter(subevent=pos_data.get('subevent'))
|
||||
if pos_data.get('variation')
|
||||
else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent')))
|
||||
new_quotas = pos_data['_quotas']
|
||||
if len(new_quotas) == 0:
|
||||
errs[i]['item'] = [gettext_lazy('The product "{}" is not assigned to a quota.').format(
|
||||
str(pos_data.get('item'))
|
||||
@@ -1219,6 +1208,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()
|
||||
@@ -1234,52 +1225,85 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
order.invoice_address = ia
|
||||
ia.last_modified = now()
|
||||
|
||||
# Generate position objects
|
||||
pos_map = {}
|
||||
for pos_data in positions_data:
|
||||
answers_data = pos_data.pop('answers', [])
|
||||
addon_to = pos_data.pop('addon_to', None)
|
||||
attendee_name = pos_data.pop('attendee_name', '')
|
||||
if attendee_name and not pos_data.get('attendee_name_parts'):
|
||||
pos_data['attendee_name_parts'] = {
|
||||
'_legacy': attendee_name
|
||||
}
|
||||
pos = OrderPosition(**pos_data)
|
||||
pos = OrderPosition(**{k: v for k, v in pos_data.items() if k != 'answers' and k != '_quotas'})
|
||||
if simulate:
|
||||
pos.order = order._wrapped
|
||||
else:
|
||||
pos.order = order
|
||||
if addon_to:
|
||||
if simulate:
|
||||
pos.addon_to = pos_map[addon_to]._wrapped
|
||||
pos.addon_to = pos_map[addon_to]
|
||||
else:
|
||||
pos.addon_to = pos_map[addon_to]
|
||||
|
||||
if pos.price is None:
|
||||
price = get_price(
|
||||
item=pos.item,
|
||||
variation=pos.variation,
|
||||
voucher=pos.voucher,
|
||||
custom_price=None,
|
||||
subevent=pos.subevent,
|
||||
addon_to=pos.addon_to,
|
||||
invoice_address=ia,
|
||||
)
|
||||
pos.price = price.gross
|
||||
pos.tax_rate = price.rate
|
||||
pos.tax_value = price.tax
|
||||
pos.tax_rule = pos.item.tax_rule
|
||||
else:
|
||||
pos._calculate_tax()
|
||||
pos_map[pos.positionid] = pos
|
||||
pos_data['__instance'] = pos
|
||||
|
||||
pos.price_before_voucher = get_price(
|
||||
item=pos.item,
|
||||
variation=pos.variation,
|
||||
voucher=None,
|
||||
custom_price=None,
|
||||
subevent=pos.subevent,
|
||||
addon_to=pos.addon_to,
|
||||
invoice_address=ia,
|
||||
).gross
|
||||
# Calculate prices if not set
|
||||
for pos_data in positions_data:
|
||||
pos = pos_data['__instance']
|
||||
if pos.addon_to_id and is_included_for_free(pos.item, pos.addon_to):
|
||||
listed_price = Decimal('0.00')
|
||||
else:
|
||||
listed_price = get_listed_price(pos.item, pos.variation, pos.subevent)
|
||||
|
||||
if pos.price is None:
|
||||
if pos.voucher:
|
||||
price_after_voucher = pos.voucher.calculate_price(listed_price)
|
||||
else:
|
||||
price_after_voucher = listed_price
|
||||
|
||||
line_price = get_line_price(
|
||||
price_after_voucher=price_after_voucher,
|
||||
custom_price_input=None,
|
||||
custom_price_input_is_net=False,
|
||||
tax_rule=pos.item.tax_rule,
|
||||
invoice_address=ia,
|
||||
bundled_sum=Decimal('0.00'),
|
||||
)
|
||||
pos.price = line_price.gross
|
||||
pos._auto_generated_price = True
|
||||
else:
|
||||
if pos.voucher:
|
||||
if not pos.item.tax_rule or pos.item.tax_rule.price_includes_tax:
|
||||
price_after_voucher = max(pos.price, pos.voucher.calculate_price(listed_price))
|
||||
else:
|
||||
price_after_voucher = max(pos.price - pos.tax_value, pos.voucher.calculate_price(listed_price))
|
||||
else:
|
||||
price_after_voucher = listed_price
|
||||
pos._auto_generated_price = False
|
||||
pos._voucher_discount = listed_price - price_after_voucher
|
||||
if pos.voucher:
|
||||
pos.voucher_budget_use = max(listed_price - price_after_voucher, Decimal('0.00'))
|
||||
|
||||
order_positions = [pos_data['__instance'] for pos_data in positions_data]
|
||||
discount_results = apply_discounts(
|
||||
self.context['event'],
|
||||
order.sales_channel,
|
||||
[
|
||||
(cp.item_id, cp.subevent_id, cp.price, bool(cp.addon_to), cp.is_bundled, pos._voucher_discount)
|
||||
for cp in order_positions
|
||||
]
|
||||
)
|
||||
for cp, (new_price, discount) in zip(order_positions, discount_results):
|
||||
if new_price != pos.price and pos._auto_generated_price:
|
||||
pos.price = new_price
|
||||
pos.discount = discount
|
||||
|
||||
# Save instances
|
||||
for pos_data in positions_data:
|
||||
answers_data = pos_data.pop('answers', [])
|
||||
pos = pos_data['__instance']
|
||||
pos._calculate_tax()
|
||||
|
||||
if simulate:
|
||||
pos = WrappedModel(pos)
|
||||
@@ -1292,6 +1316,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
answers.append(answ)
|
||||
pos.answers = answers
|
||||
pos.pseudonymization_id = "PREVIEW"
|
||||
pos_map[pos.positionid] = pos
|
||||
else:
|
||||
if pos.voucher:
|
||||
Voucher.objects.filter(pk=pos.voucher.pk).update(redeemed=F('redeemed') + 1)
|
||||
@@ -1314,10 +1339,12 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
else:
|
||||
answ = pos.answers.create(**answ_data)
|
||||
answ.options.add(*options)
|
||||
pos_map[pos.positionid] = pos
|
||||
|
||||
if not simulate:
|
||||
for cp in delete_cps:
|
||||
if cp.addon_to_id:
|
||||
continue
|
||||
cp.addons.all().delete()
|
||||
cp.delete()
|
||||
|
||||
order.total = sum([p.price for p in pos_map.values()])
|
||||
@@ -1356,14 +1383,18 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
f.order = order._wrapped if simulate else order
|
||||
f._calculate_tax()
|
||||
fees.append(f)
|
||||
if not simulate:
|
||||
if simulate:
|
||||
f.id = 0
|
||||
else:
|
||||
f.save()
|
||||
else:
|
||||
f = OrderFee(**fee_data)
|
||||
f.order = order._wrapped if simulate else order
|
||||
f._calculate_tax()
|
||||
fees.append(f)
|
||||
if not simulate:
|
||||
if simulate:
|
||||
f.id = 0
|
||||
else:
|
||||
f.save()
|
||||
|
||||
order.total += sum([f.value for f in fees])
|
||||
@@ -1426,7 +1457,7 @@ class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = InvoiceLine
|
||||
fields = ('position', 'description', 'item', 'variation', 'attendee_name', 'event_date_from',
|
||||
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')
|
||||
|
||||
|
||||
423
src/pretix/api/serializers/orderchange.py
Normal file
@@ -0,0 +1,423 @@
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
import logging
|
||||
import os
|
||||
|
||||
import pycountry
|
||||
from django.core.files import File
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from pretix.api.serializers.order import (
|
||||
AnswerCreateSerializer, AnswerSerializer, CompatibleCountryField,
|
||||
OrderPositionCreateSerializer,
|
||||
)
|
||||
from pretix.base.models import ItemVariation, Order, OrderFee, OrderPosition
|
||||
from pretix.base.services.orders import OrderError
|
||||
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OrderPositionCreateForExistingOrderSerializer(OrderPositionCreateSerializer):
|
||||
order = serializers.SlugRelatedField(slug_field='code', queryset=Order.objects.none(), required=True, allow_null=False)
|
||||
answers = AnswerCreateSerializer(many=True, required=False)
|
||||
addon_to = serializers.IntegerField(required=False, allow_null=True)
|
||||
secret = serializers.CharField(required=False)
|
||||
attendee_name = serializers.CharField(required=False, allow_null=True)
|
||||
seat = serializers.CharField(required=False, allow_null=True)
|
||||
price = serializers.DecimalField(required=False, allow_null=True, decimal_places=2,
|
||||
max_digits=10)
|
||||
country = CompatibleCountryField(source='*')
|
||||
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
fields = ('order', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
||||
'company', 'street', 'zipcode', 'city', 'country', 'state',
|
||||
'secret', 'addon_to', 'subevent', 'answers', 'seat')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if not self.context:
|
||||
return
|
||||
self.fields['order'].queryset = self.context['event'].orders.all()
|
||||
self.fields['item'].queryset = self.context['event'].items.all()
|
||||
self.fields['subevent'].queryset = self.context['event'].subevents.all()
|
||||
self.fields['seat'].queryset = self.context['event'].seats.all()
|
||||
self.fields['variation'].queryset = ItemVariation.objects.filter(item__event=self.context['event'])
|
||||
if 'order' in self.context:
|
||||
del self.fields['order']
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
if data.get('addon_to'):
|
||||
try:
|
||||
data['addon_to'] = data['order'].positions.get(positionid=data['addon_to'])
|
||||
except OrderPosition.DoesNotExist:
|
||||
raise ValidationError({
|
||||
'addon_to': ['addon_to refers to an unknown position ID for this order.']
|
||||
})
|
||||
return data
|
||||
|
||||
def create(self, validated_data):
|
||||
ocm = self.context['ocm']
|
||||
|
||||
try:
|
||||
ocm.add_position(
|
||||
item=validated_data['item'],
|
||||
variation=validated_data.get('variation'),
|
||||
price=validated_data.get('price'),
|
||||
addon_to=validated_data.get('addon_to'),
|
||||
subevent=validated_data.get('subevent'),
|
||||
seat=validated_data.get('seat'),
|
||||
)
|
||||
if self.context.get('commit', True):
|
||||
ocm.commit()
|
||||
return validated_data['order'].positions.order_by('-positionid').first()
|
||||
else:
|
||||
return OrderPosition() # fake to appease DRF
|
||||
except OrderError as e:
|
||||
raise ValidationError(str(e))
|
||||
|
||||
|
||||
class OrderPositionInfoPatchSerializer(serializers.ModelSerializer):
|
||||
answers = AnswerSerializer(many=True)
|
||||
country = CompatibleCountryField(source='*')
|
||||
attendee_name = serializers.CharField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
fields = (
|
||||
'attendee_name', 'attendee_name_parts', 'company', 'street', 'zipcode', 'city', 'country',
|
||||
'state', 'attendee_email', 'answers',
|
||||
)
|
||||
|
||||
def validate(self, data):
|
||||
if data.get('attendee_name') and data.get('attendee_name_parts'):
|
||||
raise ValidationError(
|
||||
{'attendee_name': ['Do not specify attendee_name if you specified attendee_name_parts.']}
|
||||
)
|
||||
if data.get('attendee_name_parts') and '_scheme' not in data.get('attendee_name_parts'):
|
||||
data['attendee_name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
|
||||
|
||||
if data.get('country'):
|
||||
if not pycountry.countries.get(alpha_2=data.get('country').code):
|
||||
raise ValidationError(
|
||||
{'country': ['Invalid country code.']}
|
||||
)
|
||||
|
||||
if data.get('state'):
|
||||
cc = str(data.get('country') or self.instance.country or '')
|
||||
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||
raise ValidationError(
|
||||
{'state': ['States are not supported in country "{}".'.format(cc)]}
|
||||
)
|
||||
if not pycountry.subdivisions.get(code=cc + '-' + data.get('state')):
|
||||
raise ValidationError(
|
||||
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
|
||||
)
|
||||
return data
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
answers_data = validated_data.pop('answers', None)
|
||||
|
||||
name = validated_data.pop('attendee_name', '')
|
||||
if name and not validated_data.get('attendee_name_parts'):
|
||||
validated_data['attendee_name_parts'] = {
|
||||
'_legacy': name
|
||||
}
|
||||
|
||||
for attr, value in validated_data.items():
|
||||
if attr in self.fields:
|
||||
setattr(instance, attr, value)
|
||||
|
||||
instance.save(update_fields=list(validated_data.keys()))
|
||||
|
||||
if answers_data is not None:
|
||||
qs_seen = set()
|
||||
answercache = {
|
||||
a.question_id: a for a in instance.answers.all()
|
||||
}
|
||||
for answ_data in answers_data:
|
||||
options = answ_data.pop('options', [])
|
||||
if answ_data['question'].pk in qs_seen:
|
||||
raise ValidationError(f'Question {answ_data["question"]} was sent twice.')
|
||||
if answ_data['question'].pk in answercache:
|
||||
a = answercache[answ_data['question'].pk]
|
||||
if isinstance(answ_data['answer'], File):
|
||||
a.file.save(answ_data['answer'].name, answ_data['answer'], save=False)
|
||||
a.answer = 'file://' + a.file.name
|
||||
elif a.answer.startswith('file://') and answ_data['answer'] == "file:keep":
|
||||
pass # keep current file
|
||||
else:
|
||||
for attr, value in answ_data.items():
|
||||
setattr(a, attr, value)
|
||||
a.save()
|
||||
else:
|
||||
if isinstance(answ_data['answer'], File):
|
||||
an = answ_data.pop('answer')
|
||||
a = instance.answers.create(**answ_data, answer='')
|
||||
a.file.save(os.path.basename(an.name), an, save=False)
|
||||
a.answer = 'file://' + a.file.name
|
||||
a.save()
|
||||
else:
|
||||
a = instance.answers.create(**answ_data)
|
||||
a.options.set(options)
|
||||
qs_seen.add(a.question_id)
|
||||
for qid, a in answercache.items():
|
||||
if qid not in qs_seen:
|
||||
a.delete()
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
class OrderPositionChangeSerializer(serializers.ModelSerializer):
|
||||
seat = serializers.CharField(source='seat.seat_guid', allow_null=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
fields = (
|
||||
'item', 'variation', 'subevent', 'seat', 'price', 'tax_rule',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if not self.context:
|
||||
return
|
||||
self.fields['item'].queryset = self.context['event'].items.all()
|
||||
self.fields['subevent'].queryset = self.context['event'].subevents.all()
|
||||
self.fields['tax_rule'].queryset = self.context['event'].tax_rules.all()
|
||||
if kwargs.get('partial'):
|
||||
for k, v in self.fields.items():
|
||||
self.fields[k].required = False
|
||||
|
||||
def validate_item(self, item):
|
||||
if item.event != self.context['event']:
|
||||
raise ValidationError(
|
||||
'The specified item does not belong to this event.'
|
||||
)
|
||||
return item
|
||||
|
||||
def validate_subevent(self, subevent):
|
||||
if self.context['event'].has_subevents:
|
||||
if not subevent:
|
||||
raise ValidationError(
|
||||
'You need to set a subevent.'
|
||||
)
|
||||
if subevent.event != self.context['event']:
|
||||
raise ValidationError(
|
||||
'The specified subevent does not belong to this event.'
|
||||
)
|
||||
elif subevent:
|
||||
raise ValidationError(
|
||||
'You cannot set a subevent for this event.'
|
||||
)
|
||||
return subevent
|
||||
|
||||
def validate(self, data, instance=None):
|
||||
instance = instance or self.instance
|
||||
if instance is None:
|
||||
return data # needs to be done later
|
||||
if data.get('item', instance.item):
|
||||
if data.get('item', instance.item).has_variations:
|
||||
if not data.get('variation', instance.variation):
|
||||
raise ValidationError({'variation': ['You should specify a variation for this item.']})
|
||||
else:
|
||||
if data.get('variation', instance.variation).item != data.get('item', instance.item):
|
||||
raise ValidationError(
|
||||
{'variation': ['The specified variation does not belong to the specified item.']}
|
||||
)
|
||||
elif data.get('variation', instance.variation):
|
||||
raise ValidationError(
|
||||
{'variation': ['You cannot specify a variation for this item.']}
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
ocm = self.context['ocm']
|
||||
current_seat = {'seat_guid': instance.seat.seat_guid} if instance.seat else None
|
||||
item = validated_data.get('item', instance.item)
|
||||
variation = validated_data.get('variation', instance.variation)
|
||||
subevent = validated_data.get('subevent', instance.subevent)
|
||||
price = validated_data.get('price', instance.price)
|
||||
seat = validated_data.get('seat', current_seat)
|
||||
tax_rule = validated_data.get('tax_rule', instance.tax_rule)
|
||||
|
||||
change_item = None
|
||||
if item != instance.item or variation != instance.variation:
|
||||
change_item = (item, variation)
|
||||
|
||||
change_subevent = None
|
||||
if self.context['event'].has_subevents and subevent != instance.subevent:
|
||||
change_subevent = (subevent,)
|
||||
|
||||
try:
|
||||
if change_item is not None and change_subevent is not None:
|
||||
ocm.change_item_and_subevent(instance, *change_item, *change_subevent)
|
||||
elif change_item is not None:
|
||||
ocm.change_item(instance, *change_item)
|
||||
elif change_subevent is not None:
|
||||
ocm.change_subevent(instance, *change_subevent)
|
||||
|
||||
if seat != current_seat or change_subevent:
|
||||
ocm.change_seat(instance, seat['seat_guid'] if seat else None)
|
||||
|
||||
if price != instance.price:
|
||||
ocm.change_price(instance, price)
|
||||
|
||||
if tax_rule != instance.tax_rule:
|
||||
ocm.change_tax_rule(instance, tax_rule)
|
||||
|
||||
if self.context.get('commit', True):
|
||||
ocm.commit()
|
||||
instance.refresh_from_db()
|
||||
except OrderError as e:
|
||||
raise ValidationError(str(e))
|
||||
return instance
|
||||
|
||||
|
||||
class PatchPositionSerializer(serializers.Serializer):
|
||||
position = serializers.PrimaryKeyRelatedField(queryset=OrderPosition.all.none())
|
||||
|
||||
def validate_position(self, value):
|
||||
self.fields['body'].instance = value # hack around DRFs validation order
|
||||
return value
|
||||
|
||||
def validate(self, data):
|
||||
OrderPositionChangeSerializer(context=self.context, partial=True).validate(data['body'], data['position'])
|
||||
return data
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['position'].queryset = self.context['order'].positions.all()
|
||||
self.fields['body'] = OrderPositionChangeSerializer(context=self.context, partial=True)
|
||||
|
||||
|
||||
class SelectPositionSerializer(serializers.Serializer):
|
||||
position = serializers.PrimaryKeyRelatedField(queryset=OrderPosition.all.none())
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['position'].queryset = self.context['order'].positions.all()
|
||||
|
||||
|
||||
class OrderFeeChangeSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = OrderFee
|
||||
fields = (
|
||||
'value',
|
||||
)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
ocm = self.context['ocm']
|
||||
value = validated_data.get('value', instance.value)
|
||||
|
||||
try:
|
||||
if value != instance.value:
|
||||
ocm.change_fee(instance, value)
|
||||
|
||||
if self.context.get('commit', True):
|
||||
ocm.commit()
|
||||
instance.refresh_from_db()
|
||||
except OrderError as e:
|
||||
raise ValidationError(str(e))
|
||||
return instance
|
||||
|
||||
|
||||
class PatchFeeSerializer(serializers.Serializer):
|
||||
fee = serializers.PrimaryKeyRelatedField(queryset=OrderFee.all.none())
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['fee'].queryset = self.context['order'].fees.all()
|
||||
self.fields['body'] = OrderFeeChangeSerializer(context=self.context)
|
||||
|
||||
|
||||
class SelectFeeSerializer(serializers.Serializer):
|
||||
fee = serializers.PrimaryKeyRelatedField(queryset=OrderFee.all.none())
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if not self.context:
|
||||
return
|
||||
self.fields['fee'].queryset = self.context['order'].fees.all()
|
||||
|
||||
|
||||
class OrderChangeOperationSerializer(serializers.Serializer):
|
||||
send_email = serializers.BooleanField(default=False, required=False)
|
||||
reissue_invoice = serializers.BooleanField(default=True, required=False)
|
||||
recalculate_taxes = serializers.ChoiceField(default=None, allow_null=True, required=False, choices=[
|
||||
('keep_net', 'keep_net'),
|
||||
('keep_gross', 'keep_gross'),
|
||||
])
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(self, *args, **kwargs)
|
||||
self.fields['patch_positions'] = PatchPositionSerializer(
|
||||
many=True, required=False, context=self.context
|
||||
)
|
||||
self.fields['cancel_positions'] = SelectPositionSerializer(
|
||||
many=True, required=False, context=self.context
|
||||
)
|
||||
self.fields['create_positions'] = OrderPositionCreateForExistingOrderSerializer(
|
||||
many=True, required=False, context=self.context
|
||||
)
|
||||
self.fields['split_positions'] = SelectPositionSerializer(
|
||||
many=True, required=False, context=self.context
|
||||
)
|
||||
self.fields['patch_fees'] = PatchFeeSerializer(
|
||||
many=True, required=False, context=self.context
|
||||
)
|
||||
self.fields['cancel_fees'] = SelectFeeSerializer(
|
||||
many=True, required=False, context=self.context
|
||||
)
|
||||
|
||||
def validate(self, data):
|
||||
seen_positions = set()
|
||||
for d in data.get('patch_positions', []):
|
||||
if d['position'] in seen_positions:
|
||||
raise ValidationError({'patch_positions': ['You have specified the same object twice.']})
|
||||
seen_positions.add(d['position'])
|
||||
seen_positions = set()
|
||||
for d in data.get('cancel_positions', []):
|
||||
if d['position'] in seen_positions:
|
||||
raise ValidationError({'cancel_positions': ['You have specified the same object twice.']})
|
||||
seen_positions.add(d['position'])
|
||||
seen_positions = set()
|
||||
for d in data.get('split_positions', []):
|
||||
if d['position'] in seen_positions:
|
||||
raise ValidationError({'split_positions': ['You have specified the same object twice.']})
|
||||
seen_positions.add(d['position'])
|
||||
seen_fees = set()
|
||||
for d in data.get('patch_fees', []):
|
||||
if d['fee'] in seen_fees:
|
||||
raise ValidationError({'patch_fees': ['You have specified the same object twice.']})
|
||||
seen_positions.add(d['fee'])
|
||||
seen_fees = set()
|
||||
for d in data.get('cancel_fees', []):
|
||||
if d['fee'] in seen_fees:
|
||||
raise ValidationError({'cancel_fees': ['You have specified the same object twice.']})
|
||||
seen_positions.add(d['fee'])
|
||||
|
||||
return data
|
||||
@@ -71,8 +71,22 @@ class CustomerSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Customer
|
||||
fields = ('identifier', 'email', 'name', 'name_parts', 'is_active', 'is_verified', 'last_login', 'date_joined',
|
||||
'locale', 'last_modified')
|
||||
fields = ('identifier', 'external_identifier', 'email', 'name', 'name_parts', 'is_active', 'is_verified', 'last_login', 'date_joined',
|
||||
'locale', 'last_modified', 'notes')
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
if instance and instance.provider_id:
|
||||
validated_data['external_identifier'] = instance.external_identifier
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class CustomerCreateSerializer(CustomerSerializer):
|
||||
send_email = serializers.BooleanField(default=False, required=False, allow_null=True)
|
||||
password = serializers.CharField(write_only=True, required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = Customer
|
||||
fields = CustomerSerializer.Meta.fields + ('send_email', 'password')
|
||||
|
||||
|
||||
class MembershipTypeSerializer(I18nAwareModelSerializer):
|
||||
@@ -105,20 +119,21 @@ class GiftCardSerializer(I18nAwareModelSerializer):
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
s = data['secret']
|
||||
qs = GiftCard.objects.filter(
|
||||
secret=s
|
||||
).filter(
|
||||
Q(issuer=self.context["organizer"]) | Q(
|
||||
issuer__gift_card_collector_acceptance__collector=self.context["organizer"])
|
||||
)
|
||||
if self.instance:
|
||||
qs = qs.exclude(pk=self.instance.pk)
|
||||
if qs.exists():
|
||||
raise ValidationError(
|
||||
{'secret': _(
|
||||
'A gift card with the same secret already exists in your or an affiliated organizer account.')}
|
||||
if 'secret' in data:
|
||||
s = data['secret']
|
||||
qs = GiftCard.objects.filter(
|
||||
secret=s
|
||||
).filter(
|
||||
Q(issuer=self.context["organizer"]) | Q(
|
||||
issuer__gift_card_collector_acceptance__collector=self.context["organizer"])
|
||||
)
|
||||
if self.instance:
|
||||
qs = qs.exclude(pk=self.instance.pk)
|
||||
if qs.exists():
|
||||
raise ValidationError(
|
||||
{'secret': _(
|
||||
'A gift card with the same secret already exists in your or an affiliated organizer account.')}
|
||||
)
|
||||
return data
|
||||
|
||||
class Meta:
|
||||
@@ -274,6 +289,7 @@ class TeamMemberSerializer(serializers.ModelSerializer):
|
||||
class OrganizerSettingsSerializer(SettingsSerializer):
|
||||
default_fields = [
|
||||
'customer_accounts',
|
||||
'customer_accounts_native',
|
||||
'customer_accounts_link_by_email',
|
||||
'invoice_regenerate_allowed',
|
||||
'contact_mail',
|
||||
@@ -296,7 +312,14 @@ class OrganizerSettingsSerializer(SettingsSerializer):
|
||||
'theme_round_borders',
|
||||
'primary_font',
|
||||
'organizer_logo_image_inherit',
|
||||
'organizer_logo_image'
|
||||
'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):
|
||||
|
||||